diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0c6b80e97ef5..fc61573416e8 100755 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: ppy custom: https://osu.ppy.sh/home/support diff --git a/.github/workflows_/ci.yml b/.github/workflows_/ci.yml index edbd24db82dd..39357f95f31f 100644 --- a/.github/workflows_/ci.yml +++ b/.github/workflows_/ci.yml @@ -86,9 +86,14 @@ jobs: run: | # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround. # FIXME: Suppress warnings from templates project - dotnet codefilesanity | while read -r line; do - echo "::warning::$line" - done + exit_code=0 + while read -r line; do + if [[ ! -z "$line" ]]; then + echo "::error::$line" + exit_code=1 + fi + done <<< $(dotnet codefilesanity) + exit $exit_code # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded. # - name: .NET Format (Dry Run) diff --git a/.github/workflows_/report-nunit.yml b/.github/workflows_/report-nunit.yml index e0ccd50989b3..358cbda17a45 100644 --- a/.github/workflows_/report-nunit.yml +++ b/.github/workflows_/report-nunit.yml @@ -30,3 +30,5 @@ jobs: name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}}) path: "*.trx" reporter: dotnet-trx + list-suites: 'failed' + list-tests: 'failed' diff --git a/osu.Android.props b/osu.Android.props index 84fb0fe4dff8..b5396955a96b 100755 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 3c438dc7d7bc..043ba0b927ed 100755 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -22,10 +22,8 @@ using osu.Game.Updater; using osu.Desktop.Windows; using osu.Framework.Allocation; -using osu.Framework.Graphics.Containers; using osu.Framework.Threading; using osu.Game.Configuration; -using osu.Game.Graphics; using osu.Game.IO; namespace osu.Desktop @@ -34,7 +32,6 @@ internal class OsuGameDesktop : OsuGame { private readonly bool noVersionOverlay; private VersionManager versionManager; - private TextEditIndicator textEditIndicator; private DBusManagerContainer dBusManagerContainer; public OsuGameDesktop(string[] args = null) @@ -119,8 +116,6 @@ protected override void LoadComplete() if (!noVersionOverlay) LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add); - LoadComponentAsync(textEditIndicator = new TextEditIndicator { Depth = int.MinValue }, Add); - LoadComponentAsync(new DiscordRichPresence(), Add); if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) @@ -167,33 +162,7 @@ public override void SetHost(GameHost host) desktopWindow.Title = Name; desktopWindow.DragDrop += f => fileDrop(new[] { f }); - desktopWindow.MinimumSize = new Size(600, 600); - - desktopWindow.OnTextEdit += s => - { - if (textEditIndicator != null) - { - textEditIndicator.Text = s; - - if (textEditIndicator.State.Value != Visibility.Visible) - Schedule(() => textEditIndicator.Show()); - } - }; - - desktopWindow.OnTextInput += () => - { - if (textEditIndicator != null) - { - if (textEditIndicator.State.Value != Visibility.Hidden) - { - Schedule(() => - { - textEditIndicator.Flash(); - textEditIndicator.Hide(); - }); - } - } - }; + desktopWindow.MinimumSize = new Size(800, 600); } private readonly List importableFiles = new List(); diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 518c3aa1aa75..4bb108207dd7 100755 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -9,7 +9,7 @@ osu!(lazer) lazer.ico app.manifest - 2021.1006.0 + 2021.1016.0 0.0.0 diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index 5580358f894c..2fab47f857da 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -14,11 +14,11 @@ public class CatchDifficultyCalculatorTest : DifficultyCalculatorTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; - [TestCase(4.050601681491468d, "diffcalc-test")] + [TestCase(4.0505463516206195d, "diffcalc-test")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(5.169743871843191d, "diffcalc-test")] + [TestCase(5.1696411260785498d, "diffcalc-test")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new CatchModDoubleTime()); diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs index a45877155091..d4c2c0f0af49 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs @@ -29,8 +29,7 @@ public abstract class CatchSelectionBlueprintTestScene : SelectionBlueprintTestS protected CatchSelectionBlueprintTestScene() { - EditorBeatmap = new EditorBeatmap(new CatchBeatmap()); - EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0; + EditorBeatmap = new EditorBeatmap(new CatchBeatmap()) { Difficulty = { CircleSize = 0 } }; EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 100 diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs index e3811b76692c..cca3701a6077 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; @@ -26,7 +27,7 @@ public class TestSceneBananaShowerPlacementBlueprint : CatchPlacementBlueprintTe protected override void AddHitObject(DrawableHitObject hitObject) { // Create nested bananas (but positions are not randomized because beatmap processing is not done). - hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty); + hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); base.AddHitObject(hitObject); } diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs index cd1fa31b619e..981efc9a1307 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs @@ -4,9 +4,9 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; @@ -23,11 +23,12 @@ public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTes private JuiceStream lastObject => LastObject?.HitObject as JuiceStream; - [BackgroundDependencyLoader] - private void load() + protected override IBeatmap GetPlayableBeatmap() { - Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderTickRate = 5; - Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity * 10; + var playable = base.GetPlayableBeatmap(); + playable.Difficulty.SliderTickRate = 5; + playable.Difficulty.SliderMultiplier = velocity * 10; + return playable; } [Test] diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index 5e73a89069a0..155d033dd059 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -223,7 +223,7 @@ private void addBlueprintStep(double time, float x, SliderPath sliderPath, doubl X = x, Path = sliderPath, }; - EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity; + EditorBeatmap.Difficulty.SliderMultiplier = velocity; EditorBeatmap.Add(hitObject); EditorBeatmap.Update(hitObject); Assert.That(hitObject.Velocity, Is.EqualTo(velocity)); diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 93596214db73..850e76d4bec2 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -211,7 +211,7 @@ private static void initialiseHyperDash(IBeatmap beatmap) palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); - double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; + double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2; // Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins. // This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible. diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index f155ee594992..3f41ece95e56 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -34,7 +34,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat return new CatchDifficultyAttributes { Mods = mods, Skills = skills }; // this is the same as osu!, so there's potential to share the implementation... maybe - double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; return new CatchDifficultyAttributes { @@ -69,10 +69,10 @@ protected override IEnumerable CreateDifficultyHitObjects(I protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { - halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f; + halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f; // For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay. - halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f); + halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f); return new Skill[] { diff --git a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs index 0344709d4564..9a7528d90c46 100644 --- a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs +++ b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs @@ -16,6 +16,6 @@ public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyLi { } - protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); + protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty); } } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 9fee4ae2a6b5..3737eb2cfcdd 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -42,9 +42,8 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); - double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; + double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate; diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index ba6e9224c9e9..a8ec9f1d2f4c 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -27,14 +27,14 @@ public DrawableCatchRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList new CatchFramedReplayInputHandler(replay); protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield); - protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); + protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.Difficulty); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer(); diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 538a51db5fc0..5ccb191a9b42 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; @@ -101,27 +102,27 @@ public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screen throw new System.NotImplementedException(); } - public override float GetBeatSnapDistanceAt(double referenceTime) + public override float GetBeatSnapDistanceAt(HitObject referenceObject) { throw new System.NotImplementedException(); } - public override float DurationToDistance(double referenceTime, double duration) + public override float DurationToDistance(HitObject referenceObject, double duration) { throw new System.NotImplementedException(); } - public override double DistanceToDuration(double referenceTime, float distance) + public override double DistanceToDuration(HitObject referenceObject, float distance) { throw new System.NotImplementedException(); } - public override double GetSnappedDurationFromDistance(double referenceTime, float distance) + public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) { throw new System.NotImplementedException(); } - public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) + public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) { throw new System.NotImplementedException(); } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 471dad87d541..4387bc6b3b18 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -388,7 +388,7 @@ private void performTest(List frames, Beatmap beatm }, }; - beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); } AddStep("load player", () => diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index 18891f8c5890..89e13acad6d4 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -148,7 +148,7 @@ private void performTest(List hitObjects, List fram }, }); - Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 93b5a802d7ce..0fcdd890fc93 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -42,8 +42,8 @@ public ManiaBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) { IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); - var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize); - var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + var roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize); + var roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty); if (IsForCurrentRuleset) { @@ -81,7 +81,7 @@ public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo) protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { - IBeatmapDifficultyInfo difficulty = original.BeatmapInfo.BaseDifficulty; + IBeatmapDifficultyInfo difficulty = original.Difficulty; int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate); Random = new FastRandom(seed); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index d0232dde9d88..ae527516e8ee 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -47,7 +47,7 @@ public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, Ma Debug.Assert(distanceData != null); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); - DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime); + DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint; double beatLength; #pragma warning disable 618 @@ -55,13 +55,13 @@ public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, Ma #pragma warning restore 618 beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; else - beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier; + beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity; SpanCount = repeatsData?.SpanCount() ?? 1; StartTime = (int)Math.Round(hitObject.StartTime); // This matches stable's calculation. - EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier); + EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier); SegmentDuration = (EndTime - StartTime) / SpanCount; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index d65e78bb490e..eaf0ea0f2b88 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -111,7 +111,7 @@ protected double ConversionDifficulty if (drainTime == 0) drainTime = 10000; - IBeatmapDifficultyInfo difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; + IBeatmapDifficultyInfo difficulty = OriginalBeatmap.Difficulty; conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; conversionDifficulty = Math.Min(conversionDifficulty.Value, 12); diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index fc29eadedc58..aee3268544f7 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -41,14 +41,13 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat return new ManiaDifficultyAttributes { Mods = mods, Skills = skills }; HitWindows hitWindows = new ManiaHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); return new ManiaDifficultyAttributes { StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, - // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate), + GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate), ScoreMultiplier = getScoreMultiplier(mods), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), Skills = skills diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu index 7c75b45e5ffb..ca9e5b0b8539 100644 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu @@ -13,6 +13,7 @@ SliderTickRate:1 [TimingPoints] 0,500,4,1,0,100,1,0 +10000,-150,4,1,0,100,1,0 [HitObjects] 51,192,500,128,0,1500:1:0:0:0: diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index b469487c8f2c..310d97730637 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -90,11 +90,11 @@ private void load() { // Mania doesn't care about global velocity p.Velocity = 1; - p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier; + p.BaseBeatLength *= Beatmap.Difficulty.SliderMultiplier; // For non-mania beatmap, speed changes should only happen through timing points if (!isForCurrentRuleset) - p.DifficultyPoint = new DifficultyControlPoint(); + p.EffectPoint = new EffectControlPoint(); } BarLines.ForEach(Playfield.Add); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 9af2a994709d..ef43c3a6968b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -11,6 +11,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; @@ -45,7 +46,7 @@ public TestSceneOsuDistanceSnapGrid() [SetUp] public void Setup() => Schedule(() => { - editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1; + editorBeatmap.Difficulty.SliderMultiplier = 1; editorBeatmap.ControlPointInfo.Clear(); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); @@ -179,15 +180,15 @@ public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosi public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); - public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length; + public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length; - public float DurationToDistance(double referenceTime, double duration) => (float)duration; + public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration; - public double DistanceToDuration(double referenceTime, float distance) => distance; + public double DistanceToDuration(HitObject referenceObject, float distance) => distance; - public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0; + public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0; - public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0; + public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0; } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs index 0ba775e5c7fb..37f1a846adbc 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs @@ -45,8 +45,8 @@ private void runSpmTest(Mod mod) { new Spinner { - Duration = 2000, - Position = OsuPlayfield.BASE_SIZE / 2 + Duration = 6000, + Position = OsuPlayfield.BASE_SIZE / 2, } } }, diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index af64be78f818..ed9da36b05b2 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -120,7 +120,7 @@ public void TestWithSliderReuse() => CreateModTest(new ModTestData private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4; private bool objectWithIncreasedVisibilityHasIndex(int index) - => Player.Mods.Value.OfType().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index]; + => Player.GameplayState.Mods.OfType().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index]; private class TestOsuModHidden : OsuModHidden { diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 19881b5c33af..15675e74d11b 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,13 +15,13 @@ public class OsuDifficultyCalculatorTest : DifficultyCalculatorTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; - [TestCase(6.6634445062299665d, "diffcalc-test")] - [TestCase(1.0414203870195022d, "zero-length-sliders")] + [TestCase(6.5867229481955389d, "diffcalc-test")] + [TestCase(1.0416315570967911d, "zero-length-sliders")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(8.3858089051603368d, "diffcalc-test")] - [TestCase(1.2723279173428435d, "zero-length-sliders")] + [TestCase(8.2730989071947896d, "diffcalc-test")] + [TestCase(1.2726413186221039d, "zero-length-sliders")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index 41d9bf713280..1f01ba601b0a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -59,8 +59,8 @@ public TestSceneGameplayCursor() AddSliderStep("circle size", 0f, 10f, 0f, val => { config.SetValue(OsuSetting.AutoCursorSize, true); - gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = val; - Scheduler.AddOnce(() => loadContent(false)); + gameplayState.Beatmap.Difficulty.CircleSize = val; + Scheduler.AddOnce(loadContent); }); AddStep("test cursor container", () => loadContent(false)); @@ -75,10 +75,10 @@ public TestSceneGameplayCursor() public void TestSizing(int circleSize, float userScale) { AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale)); - AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize); + AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize); AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true)); - AddStep("load content", () => loadContent()); + AddStep("load content", loadContent); AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale); @@ -98,7 +98,9 @@ public void TestTopLeftOrigin() AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin()))); } - private void loadContent(bool automated = true, Func skinProvider = null) + private void loadContent() => loadContent(false); + + private void loadContent(bool automated, Func skinProvider = null) { SetContents(_ => { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index af67ab5839d7..a5629119b615 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -32,7 +32,7 @@ public void TestMissViaEarlyHit() }; var hitWindows = new OsuHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); CreateModTest(new ModTestData { @@ -55,7 +55,7 @@ public void TestMissViaNotHitting() }; var hitWindows = new OsuHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); CreateModTest(new ModTestData { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index cfce80a2b214..d31e7a31f517 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -400,15 +400,13 @@ private void performTest(List hitObjects, List frames Beatmap.Value = CreateWorkingBeatmap(new Beatmap { HitObjects = hitObjects, + Difficulty = new BeatmapDifficulty { SliderTickRate = 3 }, BeatmapInfo = { - BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, Ruleset = new OsuRuleset().RulesetInfo }, }); - Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); - SelectedMods.Value = new[] { new OsuModClassic() }; var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); @@ -439,6 +437,8 @@ private class TestSlider : Slider { public TestSlider() { + DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }; + DefaultsApplied += _ => { HeadCircle.HitWindows = new TestHitWindows(); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 81902c25af10..03b4254eedb6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mods; using System.Linq; using NUnit.Framework; +using osu.Game.Beatmaps.Legacy; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Judgements; @@ -328,10 +329,14 @@ private Drawable createCatmull(int repeats = 0) private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier) { - var cpi = new ControlPointInfo(); - cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier }); + var cpi = new LegacyControlPointInfo(); + cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier }); - slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 }); + slider.ApplyDefaults(cpi, new BeatmapDifficulty + { + CircleSize = circleSize, + SliderTickRate = 3 + }); var drawable = CreateDrawableSlider(slider); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 590d15930060..f3392724ec3d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -348,6 +348,7 @@ private void performTest(List frames) { StartTime = time_slider_start, Position = new Vector2(0, 0), + DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }, Path = new SliderPath(PathType.PerfectCurve, new[] { Vector2.Zero, @@ -362,8 +363,6 @@ private void performTest(List frames) }, }); - Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); - var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); p.OnLoadComplete += _ => diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index fd4494ea6cf4..a243aba27ab6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -29,6 +29,9 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneSpinnerRotation : TestSceneOsuPlayer { + private const double spinner_start_time = 100; + private const double spinner_duration = 6000; + [Resolved] private AudioManager audioManager { get; set; } @@ -68,7 +71,7 @@ public void TestSpinnerMiddleRewindingRotation() { double finalAbsoluteDiscRotation = 0, finalRelativeDiscRotation = 0, finalSpinnerSymbolRotation = 0; - addSeekStep(5000); + addSeekStep(spinner_start_time + 5000); AddStep("retrieve disc rotation", () => { finalTrackerRotation = drawableSpinner.RotationTracker.Rotation; @@ -81,8 +84,8 @@ public void TestSpinnerMiddleRewindingRotation() }); AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation); - addSeekStep(2500); - AddUntilStep("disc rotation rewound", + addSeekStep(spinner_start_time + 2500); + AddAssert("disc rotation rewound", // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in. // due to the exponential damping applied we're allowing a larger margin of error of about 10% // (5% relative to the final rotation value, but we're half-way through the spin). @@ -93,7 +96,7 @@ public void TestSpinnerMiddleRewindingRotation() // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); - addSeekStep(5000); + addSeekStep(spinner_start_time + 5000); AddAssert("is disc rotation almost same", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalRelativeDiscRotation, 100)); AddAssert("is symbol rotation almost same", @@ -131,7 +134,7 @@ public void TestRotationDirection([Values(true, false)] bool clockwise) [Test] public void TestSpinnerNormalBonusRewinding() { - addSeekStep(1000); + addSeekStep(spinner_start_time + 1000); AddAssert("player score matching expected bonus score", () => { @@ -192,24 +195,9 @@ public void TestSpinUnaffectedByClockRate(double rate) AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0)); } - private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay - { - Frames = scoreReplay - .Frames - .Cast() - .Select(replayFrame => - { - var adjustedTime = replayFrame.Time * rate; - return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray()); - }) - .Cast() - .ToList() - }; - private void addSeekStep(double time) { AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } @@ -232,7 +220,8 @@ private void transformReplay(Func replayTransformation) => AddSt new Spinner { Position = new Vector2(256, 192), - EndTime = 6000, + StartTime = spinner_start_time, + Duration = spinner_duration }, } }; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 1b85e0efdeeb..2d43e1b95ec3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -369,8 +369,6 @@ private void performTest(List hitObjects, List frames }, }); - Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); - var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); p.OnLoadComplete += _ => @@ -399,6 +397,8 @@ private class TestSlider : Slider { public TestSlider() { + DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }; + DefaultsApplied += _ => { HeadCircle.HitWindows = new TestHitWindows(); diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index a2fc4848af82..d82186fb5273 100755 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -11,6 +11,7 @@ using System.Threading; using osu.Game.Rulesets.Osu.UI; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps.Legacy; namespace osu.Game.Rulesets.Osu.Beatmaps { @@ -44,7 +45,7 @@ protected override IEnumerable ConvertHitObject(HitObject original LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset, // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // this results in more (or less) ticks being generated in h is OsuModRelax)) + speedRating = 0.0; + double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000; double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000; double baseFlashlightPerformance = 0.0; @@ -53,7 +56,8 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double drainRate = beatmap.Difficulty.DrainRate; int maxCombo = beatmap.HitObjects.Count; // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) @@ -71,6 +75,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat FlashlightRating = flashlightRating, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, + DrainRate = drainRate, MaxCombo = maxCombo, HitCircleCount = hitCirclesCount, SpinnerCount = spinnerCount, @@ -95,10 +100,9 @@ protected override IEnumerable CreateDifficultyHitObjects(I protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { HitWindows hitWindows = new OsuHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future - hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate; + hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; return new Skill[] { diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 9cc8aeb52c66..d17651904583 100755 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -40,15 +40,21 @@ public override double Calculate(Dictionary categoryRatings = nu countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - // Custom multipliers for NoFail and SpunOut. double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. + // Custom multipliers for NoFail and SpunOut. if (mods.Any(m => m is OsuModNoFail)) multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss); if (mods.Any(m => m is OsuModSpunOut)) multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); + if (mods.Any(h => h is OsuModRelax)) + { + countMiss += countOk + countMeh; + multiplier *= 0.6; + } + double aimValue = computeAimValue(); double speedValue = computeSpeedValue(); double accuracyValue = computeAccuracyValue(); @@ -108,9 +114,13 @@ private double computeAimValue() double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; - // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. - if (mods.Any(h => h is OsuModHidden)) + if (mods.Any(m => m is OsuModBlinds)) + aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); + else if (mods.Any(h => h is OsuModHidden)) + { + // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); + } aimValue *= approachRateBonus; @@ -147,11 +157,20 @@ private double computeSpeedValue() speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; - if (mods.Any(m => m is OsuModHidden)) + if (mods.Any(m => m is OsuModBlinds)) + { + // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. + speedValue *= 1.12; + } + else if (mods.Any(m => m is OsuModHidden)) + { + // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); + } // Scale the speed value with accuracy and OD. speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); + // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); @@ -160,6 +179,9 @@ private double computeSpeedValue() private double computeAccuracyValue() { + if (mods.Any(h => h is OsuModRelax)) + return 0.0; + // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. double betterAccuracyPercentage; int amountHitObjectsWithAccuracy = Attributes.HitCircleCount; @@ -180,8 +202,12 @@ private double computeAccuracyValue() // Bonus for many hitcircles - it's harder to keep good accuracy up for longer. accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); - if (mods.Any(m => m is OsuModHidden)) + // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. + if (mods.Any(m => m is OsuModBlinds)) + accuracyValue *= 1.14; + else if (mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; + if (mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 8e8f9bc06eb4..5e5993aefec5 100755 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -54,6 +54,10 @@ public OsuDifficultyHitObject(HitObject hitObject, HitObject lastLastObject, Hit private void setDistances() { + // We don't need to calculate either angle or distance when one of the last->curr objects is a spinner + if (BaseObject is Spinner || lastObject is Spinner) + return; + // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. float scalingFactor = normalized_radius / (float)BaseObject.Radius; @@ -71,11 +75,9 @@ private void setDistances() Vector2 lastCursorPosition = getEndCursorPosition(lastObject); - // Don't need to jump to reach spinners - if (!(BaseObject is Spinner)) - JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; + JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; - if (lastLastObject != null) + if (lastLastObject != null && !(lastLastObject is Spinner)) { Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastObject); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 16a18cbcb90c..d8f4aa1229f2 100755 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -22,17 +22,19 @@ public Aim(Mod[] mods) { } - protected override double SkillMultiplier => 26.25; - protected override double StrainDecayBase => 0.15; + private double currentStrain = 1; - protected override double StrainValueOf(DifficultyHitObject current) + private double skillMultiplier => 26.25; + private double strainDecayBase => 0.15; + + private double strainValueOf(DifficultyHitObject current) { if (current.BaseObject is Spinner) return 0; var osuCurrent = (OsuDifficultyHitObject)current; - double result = 0; + double aimStrain = 0; if (Previous.Count > 0) { @@ -46,7 +48,7 @@ protected override double StrainValueOf(DifficultyHitObject current) Math.Max(osuPrevious.JumpDistance - scale, 0) * Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2) * Math.Max(osuCurrent.JumpDistance - scale, 0)); - result = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime); + aimStrain = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime); } } @@ -54,11 +56,23 @@ protected override double StrainValueOf(DifficultyHitObject current) double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance); return Math.Max( - result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold), + aimStrain + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold), (Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime ); } private double applyDiminishingExp(double val) => Math.Pow(val, 0.99); + + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); + + protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime); + + protected override double StrainValueAt(DifficultyHitObject current) + { + currentStrain *= strainDecay(current.DeltaTime); + currentStrain += strainValueOf(current) * skillMultiplier; + + return currentStrain; + } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index abd900a80d53..e3abe7d7008b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -19,12 +19,13 @@ public Flashlight(Mod[] mods) { } - protected override double SkillMultiplier => 0.15; - protected override double StrainDecayBase => 0.15; + private double skillMultiplier => 0.15; + private double strainDecayBase => 0.15; protected override double DecayWeight => 1.0; protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations. + private double currentStrain = 1; - protected override double StrainValueOf(DifficultyHitObject current) + private double strainValueOf(DifficultyHitObject current) { if (current.BaseObject is Spinner) return 0; @@ -62,5 +63,17 @@ protected override double StrainValueOf(DifficultyHitObject current) return Math.Pow(smallDistNerf * result, 2.0); } + + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); + + protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime); + + protected override double StrainValueAt(DifficultyHitObject current) + { + currentStrain *= strainDecay(current.DeltaTime); + currentStrain += strainValueOf(current) * skillMultiplier; + + return currentStrain; + } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 7bcd867a9c28..e47edc37cca9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { - public abstract class OsuStrainSkill : StrainDecaySkill + public abstract class OsuStrainSkill : StrainSkill { /// /// The number of sections with the highest strains, which the peak strain reductions will apply to. diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 9364b110484b..cae6b8e01cbf 100755 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,18 +16,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills public class Speed : OsuStrainSkill { private const double single_spacing_threshold = 125; + private const double rhythm_multiplier = 0.75; + private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max. + private const double min_speed_bonus = 75; // ~200BPM + private const double speed_balancing_factor = 40; + + private double skillMultiplier => 1375; + private double strainDecayBase => 0.3; - private const double angle_bonus_begin = 5 * Math.PI / 6; - private const double pi_over_4 = Math.PI / 4; - private const double pi_over_2 = Math.PI / 2; + private double currentStrain = 1; + private double currentRhythm = 1; - protected override double SkillMultiplier => 1400; - protected override double StrainDecayBase => 0.3; protected override int ReducedSectionCount => 5; protected override double DifficultyMultiplier => 1.04; - - private const double min_speed_bonus = 75; // ~200BPM - private const double speed_balancing_factor = 40; + protected override int HistoryLength => 32; private readonly double greatWindow; @@ -37,52 +39,138 @@ public Speed(Mod[] mods, double hitWindowGreat) greatWindow = hitWindowGreat; } - protected override double StrainValueOf(DifficultyHitObject current) + /// + /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . + /// + private double calculateRhythmBonus(DifficultyHitObject current) { if (current.BaseObject is Spinner) return 0; - var osuCurrent = (OsuDifficultyHitObject)current; - var osuPrevious = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null; + int previousIslandSize = 0; + + double rhythmComplexitySum = 0; + int islandSize = 1; + double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms + + bool firstDeltaSwitch = false; + + for (int i = Previous.Count - 2; i > 0; i--) + { + OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1]; + OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i]; + OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1]; + + double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now + + if (currHistoricalDecay != 0) + { + currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count. + + double currDelta = currObj.StrainTime; + double prevDelta = prevObj.StrainTime; + double lastDelta = lastObj.StrainTime; + double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. + + double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6)); + + windowPenalty = Math.Min(1, windowPenalty); + + double effectiveRatio = windowPenalty * currRatio; + + if (firstDeltaSwitch) + { + if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta)) + { + if (islandSize < 7) + islandSize++; // island is still progressing, count size. + } + else + { + if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window + effectiveRatio *= 0.125; + + if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle + effectiveRatio *= 0.25; + + if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) + effectiveRatio *= 0.25; + + if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5) + effectiveRatio *= 0.50; + + if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. + effectiveRatio *= 0.125; + + rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2; - double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance); - double strainTime = osuCurrent.StrainTime; + startRatio = effectiveRatio; + previousIslandSize = islandSize; // log the last island size. + + if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting + firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. + + islandSize = 1; + } + } + else if (prevDelta > 1.25 * currDelta) // we want to be speeding up. + { + // Begin counting island until we change speed again. + firstDeltaSwitch = true; + startRatio = effectiveRatio; + islandSize = 1; + } + } + } + + return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) + } + + private double strainValueOf(DifficultyHitObject current) + { + if (current.BaseObject is Spinner) + return 0; + + // derive strainTime for calculation + var osuCurrObj = (OsuDifficultyHitObject)current; + var osuPrevObj = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null; + + double strainTime = osuCurrObj.StrainTime; double greatWindowFull = greatWindow * 2; double speedWindowRatio = strainTime / greatWindowFull; // Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between) - if (osuPrevious != null && strainTime < greatWindowFull && osuPrevious.StrainTime > strainTime) - strainTime = Interpolation.Lerp(osuPrevious.StrainTime, strainTime, speedWindowRatio); + if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime) + strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio); // Cap deltatime to the OD 300 hitwindow. // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1); + // derive speedBonus for calculation double speedBonus = 1.0; + if (strainTime < min_speed_bonus) - speedBonus = 1 + Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); + speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); - double angleBonus = 1.0; + double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance); - if (osuCurrent.Angle != null && osuCurrent.Angle.Value < angle_bonus_begin) - { - angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57; + return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime; + } - if (osuCurrent.Angle.Value < pi_over_2) - { - angleBonus = 1.28; - if (distance < 90 && osuCurrent.Angle.Value < pi_over_4) - angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1); - else if (distance < 90) - angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - osuCurrent.Angle.Value) / pi_over_4); - } - } + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); + + protected override double CalculateInitialStrain(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime); + + protected override double StrainValueAt(DifficultyHitObject current) + { + currentStrain *= strainDecay(current.DeltaTime); + currentStrain += strainValueOf(current) * skillMultiplier; + + currentRhythm = calculateRhythmBonus(current); - return (1 + (speedBonus - 1) * 0.75) - * angleBonus - * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) - / strainTime; + return currentStrain * currentRhythm; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index cf771b7ceabb..974b5e792546 100755 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -8,12 +8,14 @@ using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Screens.Edit; using osuTK; using osuTK.Input; @@ -67,6 +69,9 @@ protected override void LoadComplete() inputManager = GetContainingInputManager(); } + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } + public override void UpdateTimeAndPosition(SnapResult result) { base.UpdateTimeAndPosition(result); @@ -75,6 +80,10 @@ public override void UpdateTimeAndPosition(SnapResult result) { case SliderPlacementState.Initial: BeginPlacement(); + + var nearestDifficultyPoint = editorBeatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint; + + HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint(); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); break; @@ -212,7 +221,7 @@ private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint) private void updateSlider() { - HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index f684a32df713..95094886ff16 100755 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -230,7 +230,7 @@ private void removeControlPoints(List toRemove) private void updatePath() { - HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; editorBeatmap?.Update(HitObject); } diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs index 0d0c3d9e6944..f0aade1b7fc8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs @@ -19,7 +19,7 @@ public class CheckTooShortSpinners : ICheck public IEnumerable Run(BeatmapVerifierContext context) { - double od = context.Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty; + double od = context.Beatmap.Difficulty.OverallDifficulty; // These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner. // It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners. diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs index ff3be974273e..8a561f962ab5 100755 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit public class OsuDistanceSnapGrid : CircularDistanceSnapGrid { public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null) - : base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime) + : base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime) { Masking = true; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs new file mode 100644 index 000000000000..c48cbd999292 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs @@ -0,0 +1,76 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.Mods; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Bindables; +using osu.Framework.Localisation; +using osu.Framework.Utils; +using osu.Game.Graphics.UserInterface; +using osu.Game.Configuration; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModNoScope : Mod, IUpdatableByPlayfield, IApplicableToScoreProcessor + { + /// + /// Slightly higher than the cutoff for . + /// + private const float min_alpha = 0.0002f; + + private const float transition_duration = 100; + + public override string Name => "No Scope"; + public override string Acronym => "NS"; + public override ModType Type => ModType.Fun; + public override IconUsage? Icon => FontAwesome.Solid.EyeSlash; + public override string Description => "Where's the cursor?"; + public override double ScoreMultiplier => 1; + + private BindableNumber currentCombo; + + private float targetAlpha; + + [SettingSource( + "Hidden at combo", + "The combo count at which the cursor becomes completely hidden", + SettingControlType = typeof(SettingsSlider) + )] + public BindableInt HiddenComboCount { get; } = new BindableInt + { + Default = 10, + Value = 10, + MinValue = 0, + MaxValue = 50, + }; + + public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; + + public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + { + if (HiddenComboCount.Value == 0) return; + + currentCombo = scoreProcessor.Combo.GetBoundCopy(); + currentCombo.BindValueChanged(combo => + { + targetAlpha = Math.Max(min_alpha, 1 - (float)combo.NewValue / HiddenComboCount.Value); + }, true); + } + + public virtual void Update(Playfield playfield) + { + playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / transition_duration, 0, 1)); + } + } + + public class HiddenComboSlider : OsuSliderBar + { + public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText; + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 9ef2aab7ef44..d70abfcf5f65 100755 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -175,7 +175,7 @@ public override void ApplyToBeatmap(IBeatmap beatmap) .Select(beat => { var newCircle = new HitCircle(); - newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); + newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.Difficulty); newCircle.StartTime = beat; return (OsuHitObject)newCircle; }).ToList(); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 1d2666f46bbb..07d03ee1ebb0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -140,9 +140,8 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); - double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; + double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; @@ -175,7 +174,6 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok StartTime = e.Time, Position = Position, StackHeight = StackHeight, - SampleControlPoint = SampleControlPoint, }); break; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 0ae6e84b118e..f42c4d821d83 100755 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -193,6 +193,7 @@ public override IEnumerable GetModsFor(ModType type) new OsuModBarrelRoll(), new OsuModApproachDifferent(), new OsuModMuted(), + new OsuModNoScope(), }; case ModType.System: diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index b88bf9108bb1..e231550e3ebd 100755 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -50,7 +50,7 @@ public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList mods) : base(beatmap, mods) { defaultHitWindows = new OsuHitWindows(); - defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + defaultHitWindows.SetDifficulty(Beatmap.Difficulty.OverallDifficulty); } #endregion diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index cb769c31b83c..24a660e69e48 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -179,7 +179,7 @@ private void load() return; // Todo: This should probably not be done like this. - float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2; + float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.Difficulty.CircleSize - 5) / 5) / 2; foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle))) { diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 437599d4976b..5bcdb6a97774 100755 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -99,7 +99,7 @@ private void calculateScale() if (autoCursorScale.Value && state != null) { // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. - scale *= GetScaleForCircleSize(state.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize); + scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize); } cursorScale.Value = scale; diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index dd3c6b317ad4..4b0b74ad2743 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ public class TaikoDifficultyCalculatorTest : DifficultyCalculatorTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.2867022617692685d, "diffcalc-test")] - [TestCase(2.2867022617692685d, "diffcalc-test-strong")] + [TestCase(2.2420075288523802d, "diffcalc-test")] + [TestCase(2.2420075288523802d, "diffcalc-test-strong")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(3.1704781712282624d, "diffcalc-test")] - [TestCase(3.1704781712282624d, "diffcalc-test-strong")] + [TestCase(3.134084469440479d, "diffcalc-test")] + [TestCase(3.134084469440479d, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index d6a69a49af88..79948859e854 100755 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -10,6 +10,7 @@ using System.Linq; using osu.Framework.Utils; using System.Threading; +using JetBrains.Annotations; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; @@ -46,11 +47,10 @@ public TaikoBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { - if (!(original.BeatmapInfo.BaseDifficulty is TaikoMutliplierAppliedDifficulty)) + if (!(original.Difficulty is TaikoMultiplierAppliedDifficulty)) { // Rewrite the beatmap info to add the slider velocity multiplier - original.BeatmapInfo = original.BeatmapInfo.Clone(); - original.BeatmapInfo.BaseDifficulty = new TaikoMutliplierAppliedDifficulty(original.BeatmapInfo.BaseDifficulty); + original.Difficulty = new TaikoMultiplierAppliedDifficulty(original.Difficulty); } Beatmap converted = base.ConvertBeatmap(original, cancellationToken); @@ -108,7 +108,7 @@ protected override IEnumerable ConvertHitObject(HitObject obj, I StartTime = obj.StartTime, Samples = obj.Samples, Duration = taikoDuration, - TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4 + TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4 }; } @@ -117,7 +117,7 @@ protected override IEnumerable ConvertHitObject(HitObject obj, I case IHasDuration endTimeData: { - double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; + double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; yield return new Swell { @@ -154,7 +154,7 @@ private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDist double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); - DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime); + DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint; double beatLength; #pragma warning disable 618 @@ -162,12 +162,12 @@ private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDist #pragma warning restore 618 beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; else - beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier; + beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity; - double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate; + double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate; // The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll. - double taikoVelocity = sliderScoringPointDistance * beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate; + double taikoVelocity = sliderScoringPointDistance * beatmap.Difficulty.SliderTickRate; taikoDuration = (int)(distance / taikoVelocity * beatLength); if (isForCurrentRuleset) @@ -183,7 +183,7 @@ private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDist beatLength = timingPoint.BeatLength; // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat - tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, (double)taikoDuration / spans); + tickSpacing = Math.Min(beatLength / beatmap.Difficulty.SliderTickRate, (double)taikoDuration / spans); return tickSpacing > 0 && distance / osuVelocity * 1000 < 2 * beatLength; @@ -191,14 +191,35 @@ private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDist protected override Beatmap CreateBeatmap() => new TaikoBeatmap(); - private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty + private class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty { - public TaikoMutliplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty) + public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty) { CopyFrom(difficulty); + } + + [UsedImplicitly] + public TaikoMultiplierAppliedDifficulty() + { + } + + #region Overrides of BeatmapDifficulty - SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; + public override void CopyTo(BeatmapDifficulty other) + { + base.CopyTo(other); + if (!(other is TaikoMultiplierAppliedDifficulty)) + SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; } + + public override void CopyFrom(IBeatmapDifficultyInfo other) + { + base.CopyFrom(other); + if (!(other is TaikoMultiplierAppliedDifficulty)) + SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; + } + + #endregion } } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 18d06c069fa4..7dd47e804b27 100755 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -85,7 +85,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat starRating = rescale(starRating); HitWindows hitWindows = new TaikoHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); return new TaikoDifficultyAttributes { @@ -94,8 +94,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat StaminaStrain = staminaRating, RhythmStrain = rhythmRating, ColourStrain = colourRating, - // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate, + GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), Skills = skills }; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 0318e329911b..0e93ad7e73ba 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -63,9 +63,8 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); - double scoringDistance = base_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; + double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; tickSpacing = timingPoint.BeatLength / TickRate; diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index 94cd411d7be0..0d6ce44255ce 100755 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -40,8 +40,8 @@ public override void ApplyBeatmap(IBeatmap beatmap) { base.ApplyBeatmap(beatmap); - hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); - hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); + hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.5, 0.75, 0.98)); + hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.0018, 0.0075, 0.0120); } protected override double GetHealthIncreaseFor(JudgementResult result) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index a4bf8c92e38a..cb12d0362002 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -129,7 +129,7 @@ public void TestDecodeBeatmapDifficulty() using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new LineBufferedReader(resStream)) { - var difficulty = decoder.Decode(stream).BeatmapInfo.BaseDifficulty; + var difficulty = decoder.Decode(stream).Difficulty; Assert.AreEqual(6.5f, difficulty.DrainRate); Assert.AreEqual(4, difficulty.CircleSize); @@ -192,15 +192,15 @@ public void TestDecodeBeatmapTimingPoints() var difficultyPoint = controlPoints.DifficultyPointAt(0); Assert.AreEqual(0, difficultyPoint.Time); - Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier); + Assert.AreEqual(1.0, difficultyPoint.SliderVelocity); difficultyPoint = controlPoints.DifficultyPointAt(48428); Assert.AreEqual(0, difficultyPoint.Time); - Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier); + Assert.AreEqual(1.0, difficultyPoint.SliderVelocity); difficultyPoint = controlPoints.DifficultyPointAt(116999); Assert.AreEqual(116999, difficultyPoint.Time); - Assert.AreEqual(0.75, difficultyPoint.SpeedMultiplier, 0.1); + Assert.AreEqual(0.75, difficultyPoint.SliderVelocity, 0.1); var soundPoint = controlPoints.SamplePointAt(0); Assert.AreEqual(956, soundPoint.Time); @@ -227,7 +227,7 @@ public void TestDecodeBeatmapTimingPoints() Assert.IsTrue(effectPoint.KiaiMode); Assert.IsFalse(effectPoint.OmitFirstBarLine); - effectPoint = controlPoints.EffectPointAt(119637); + effectPoint = controlPoints.EffectPointAt(116637); Assert.AreEqual(95901, effectPoint.Time); Assert.IsFalse(effectPoint.KiaiMode); Assert.IsFalse(effectPoint.OmitFirstBarLine); @@ -249,10 +249,10 @@ public void TestDecodeOverlappingTimingPoints() Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(3)); Assert.That(controlPoints.SamplePoints.Count, Is.EqualTo(3)); - Assert.That(controlPoints.DifficultyPointAt(500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1)); - Assert.That(controlPoints.DifficultyPointAt(1500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1)); - Assert.That(controlPoints.DifficultyPointAt(2500).SpeedMultiplier, Is.EqualTo(0.75).Within(0.1)); - Assert.That(controlPoints.DifficultyPointAt(3500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(500).SliderVelocity, Is.EqualTo(1.5).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(1500).SliderVelocity, Is.EqualTo(1.5).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(2500).SliderVelocity, Is.EqualTo(0.75).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(3500).SliderVelocity, Is.EqualTo(1.5).Within(0.1)); Assert.That(controlPoints.EffectPointAt(500).KiaiMode, Is.True); Assert.That(controlPoints.EffectPointAt(1500).KiaiMode, Is.True); @@ -279,10 +279,10 @@ public void TestTimingPointResetsSpeedMultiplier() using (var resStream = TestResources.OpenResource("timingpoint-speedmultiplier-reset.osu")) using (var stream = new LineBufferedReader(resStream)) { - var controlPoints = decoder.Decode(stream).ControlPointInfo; + var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo; - Assert.That(controlPoints.DifficultyPointAt(0).SpeedMultiplier, Is.EqualTo(0.5).Within(0.1)); - Assert.That(controlPoints.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(0).SliderVelocity, Is.EqualTo(0.5).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1).Within(0.1)); } } @@ -394,12 +394,12 @@ public void TestDecodeControlPointDifficultyChange() using (var resStream = TestResources.OpenResource("controlpoint-difficulty-multiplier.osu")) using (var stream = new LineBufferedReader(resStream)) { - var controlPointInfo = decoder.Decode(stream).ControlPointInfo; + var controlPointInfo = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo; - Assert.That(controlPointInfo.DifficultyPointAt(5).SpeedMultiplier, Is.EqualTo(1)); - Assert.That(controlPointInfo.DifficultyPointAt(1000).SpeedMultiplier, Is.EqualTo(10)); - Assert.That(controlPointInfo.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1.8518518518518519d)); - Assert.That(controlPointInfo.DifficultyPointAt(3000).SpeedMultiplier, Is.EqualTo(0.5)); + Assert.That(controlPointInfo.DifficultyPointAt(5).SliderVelocity, Is.EqualTo(1)); + Assert.That(controlPointInfo.DifficultyPointAt(1000).SliderVelocity, Is.EqualTo(10)); + Assert.That(controlPointInfo.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1.8518518518518519d)); + Assert.That(controlPointInfo.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(0.5)); } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 896aa53f8297..d12da1a22f42 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -46,8 +46,7 @@ public void TestEncodeDecodeStability(string name) sort(decoded.beatmap); sort(decodedAfterEncode.beatmap); - Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize())); - Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration)); + compareBeatmaps(decoded, decodedAfterEncode); } [TestCaseSource(nameof(allBeatmaps))] @@ -62,8 +61,7 @@ public void TestEncodeDecodeStabilityDoubleConvert(string name) sort(decoded.beatmap); sort(decodedAfterEncode.beatmap); - Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize())); - Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration)); + compareBeatmaps(decoded, decodedAfterEncode); } [TestCaseSource(nameof(allBeatmaps))] @@ -77,12 +75,7 @@ public void TestEncodeDecodeStabilityWithNonLegacyControlPoints(string name) var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name); - // in this process, we may lose some detail in the control points section. - // let's focus on only the hitobjects. - var originalHitObjects = decoded.beatmap.HitObjects.Serialize(); - var newHitObjects = decodedAfterEncode.beatmap.HitObjects.Serialize(); - - Assert.That(newHitObjects, Is.EqualTo(originalHitObjects)); + compareBeatmaps(decoded, decodedAfterEncode); ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo) { @@ -97,7 +90,7 @@ ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo // completely ignore "legacy" types, which have been moved to HitObjects. // even though these would mostly be ignored by the Add call, they will still be available in groups, // which isn't what we want to be testing here. - if (point is SampleControlPoint) + if (point is SampleControlPoint || point is DifficultyControlPoint) continue; newControlPoints.Add(point.Time, point.DeepClone()); @@ -107,6 +100,19 @@ ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo } } + private void compareBeatmaps((IBeatmap beatmap, TestLegacySkin skin) expected, (IBeatmap beatmap, TestLegacySkin skin) actual) + { + // Check all control points that are still considered to be at a global level. + Assert.That(expected.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.TimingPoints.Serialize())); + Assert.That(expected.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.EffectPoints.Serialize())); + + // Check all hitobjects. + Assert.That(expected.beatmap.HitObjects.Serialize(), Is.EqualTo(actual.beatmap.HitObjects.Serialize())); + + // Check skin. + Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration)); + } + [Test] public void TestEncodeMultiSegmentSliderWithFloatingPointError() { @@ -156,7 +162,7 @@ private void sort(IBeatmap beatmap) } } - private (IBeatmap beatmap, TestLegacySkin beatmapSkin) decodeFromLegacy(Stream stream, string name) + private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name) { using (var reader = new LineBufferedReader(stream)) { @@ -174,7 +180,7 @@ public TestLegacySkin(IResourceStore storage, string fileName) } } - private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap) + private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin skin) fullBeatmap) { var (beatmap, beatmapSkin) = fullBeatmap; var stream = new MemoryStream(); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 1fc3abef9ad4..9ec2f375694b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -84,7 +84,7 @@ public void TestDecodeEditor() public void TestDecodeDifficulty() { var beatmap = decodeAsJson(normal); - var difficulty = beatmap.BeatmapInfo.BaseDifficulty; + var difficulty = beatmap.Difficulty; Assert.AreEqual(6.5f, difficulty.DrainRate); Assert.AreEqual(4, difficulty.CircleSize); Assert.AreEqual(8, difficulty.OverallDifficulty); @@ -102,7 +102,7 @@ public void TestDecodePostConverted() processor.PreProcess(); foreach (var o in converted.HitObjects) - o.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); + o.ApplyDefaults(converted.ControlPointInfo, converted.Difficulty); processor.PostProcess(); var beatmap = converted.Serialize().Deserialize(); diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index a147b3ceb622..9a9ecd2a3708 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -513,5 +513,17 @@ public void TestRelativeExternalLinks() Assert.AreEqual(LinkAction.External, result.Action); Assert.AreEqual("/relative", result.Argument); } + + [TestCase("https://dev.ppy.sh/home/changelog", "")] + [TestCase("https://dev.ppy.sh/home/changelog/lazer/2021.1012", "lazer/2021.1012")] + public void TestChangelogLinks(string link, string expectedArg) + { + MessageFormatter.WebsiteRootUrl = "dev.ppy.sh"; + + LinkDetails result = MessageFormatter.GetLinkDetails(link); + + Assert.AreEqual(LinkAction.OpenChangelog, result.Action); + Assert.AreEqual(expectedArg, result.Argument); + } } } diff --git a/osu.Game.Tests/Database/FileStoreTests.cs b/osu.Game.Tests/Database/FileStoreTests.cs new file mode 100644 index 000000000000..861de5303d1f --- /dev/null +++ b/osu.Game.Tests/Database/FileStoreTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Logging; +using osu.Game.Models; +using osu.Game.Stores; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + public class FileStoreTests : RealmTest + { + [Test] + public void TestImportFile() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); + + realm.Write(() => files.Add(testData, realm)); + + Assert.True(files.Storage.Exists("0/05/054edec1d0211f624fed0cbca9d4f9400b0e491c43742af2c5b0abebf0c990d8")); + Assert.True(files.Storage.Exists(realm.All().First().StoragePath)); + }); + } + + [Test] + public void TestImportSameFileTwice() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); + + realm.Write(() => files.Add(testData, realm)); + realm.Write(() => files.Add(testData, realm)); + + Assert.AreEqual(1, realm.All().Count()); + }); + } + + [Test] + public void TestDontPurgeReferenced() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); + + var timer = new Stopwatch(); + timer.Start(); + + realm.Write(() => + { + // attach the file to an arbitrary beatmap + var beatmapSet = CreateBeatmapSet(CreateRuleset()); + + beatmapSet.Files.Add(new RealmNamedFileUsage(file, "arbitrary.resource")); + + realm.Add(beatmapSet); + }); + + Logger.Log($"Import complete at {timer.ElapsedMilliseconds}"); + + string path = file.StoragePath; + + Assert.True(realm.All().Any()); + Assert.True(files.Storage.Exists(path)); + + files.Cleanup(); + Logger.Log($"Cleanup complete at {timer.ElapsedMilliseconds}"); + + Assert.True(realm.All().Any()); + Assert.True(file.IsValid); + Assert.True(files.Storage.Exists(path)); + }); + } + + [Test] + public void TestPurgeUnreferenced() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); + + string path = file.StoragePath; + + Assert.True(realm.All().Any()); + Assert.True(files.Storage.Exists(path)); + + files.Cleanup(); + + Assert.False(realm.All().Any()); + Assert.False(file.IsValid); + Assert.False(files.Storage.Exists(path)); + }); + } + } +} diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 245981cd9b62..3e8b6091fdfd 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System; using System.Threading; using System.Threading.Tasks; diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs new file mode 100644 index 000000000000..33aa1afb89da --- /dev/null +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -0,0 +1,213 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Models; +using Realms; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + public class RealmLiveTests : RealmTest + { + [Test] + public void TestLiveCastability() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(); + + ILive iBeatmap = beatmap; + + Assert.AreEqual(0, iBeatmap.Value.Length); + }); + } + + [Test] + public void TestValueAccessWithOpenContext() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + Assert.DoesNotThrow(() => + { + using (realmFactory.CreateContext()) + { + var resolved = liveBeatmap.Value; + + Assert.IsTrue(resolved.Realm.IsClosed); + Assert.IsTrue(resolved.IsValid); + + // can access properties without a crash. + Assert.IsFalse(resolved.Hidden); + } + }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestScopedReadWithoutContext() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + liveBeatmap.PerformRead(beatmap => + { + Assert.IsTrue(beatmap.IsValid); + Assert.IsFalse(beatmap.Hidden); + }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestScopedWriteWithoutContext() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; }); + liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestValueAccessWithoutOpenContextFails() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + Assert.Throws(() => + { + var unused = liveBeatmap.Value; + }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestLiveAssumptions() + { + RunTestWithRealm((realmFactory, _) => + { + int changesTriggered = 0; + + using (var updateThreadContext = realmFactory.CreateContext()) + { + updateThreadContext.All().SubscribeForNotifications(gotChange); + RealmLive? liveBeatmap = null; + + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var ruleset = CreateRuleset(); + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + // add a second beatmap to ensure that a full refresh occurs below. + // not just a refresh from the resolved Live. + threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + // not yet seen by main context + Assert.AreEqual(0, updateThreadContext.All().Count()); + Assert.AreEqual(0, changesTriggered); + + var resolved = liveBeatmap.Value; + + // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. + Assert.AreEqual(2, updateThreadContext.All().Count()); + Assert.AreEqual(1, changesTriggered); + + // even though the realm that this instance was resolved for was closed, it's still valid. + Assert.IsTrue(resolved.Realm.IsClosed); + Assert.IsTrue(resolved.IsValid); + + // can access properties without a crash. + Assert.IsFalse(resolved.Hidden); + + updateThreadContext.Write(r => + { + // can use with the main context. + r.Remove(resolved); + }); + } + + void gotChange(IRealmCollection sender, ChangeSet changes, Exception error) + { + changesTriggered++; + } + }); + } + } +} diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 576f901c1ae9..04c9f2577a10 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -4,12 +4,13 @@ using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -using Nito.AsyncEx; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.Models; #nullable enable @@ -28,42 +29,109 @@ static RealmTest() protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") { - AsyncContext.Run(() => + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller)) { - var testStorage = storage.GetStorageForDirectory(caller); - - using (var realmFactory = new RealmContextFactory(testStorage, caller)) + host.Run(new RealmTestGame(() => { - Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); - testAction(realmFactory, testStorage); + var testStorage = storage.GetStorageForDirectory(caller); - realmFactory.Dispose(); + using (var realmFactory = new RealmContextFactory(testStorage, caller)) + { + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); + testAction(realmFactory, testStorage); - Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); - realmFactory.Compact(); - Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); - } - }); + realmFactory.Dispose(); + + Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); + realmFactory.Compact(); + Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); + } + })); + } } protected void RunTestWithRealmAsync(Func testAction, [CallerMemberName] string caller = "") { - AsyncContext.Run(async () => + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller)) { - var testStorage = storage.GetStorageForDirectory(caller); - - using (var realmFactory = new RealmContextFactory(testStorage, caller)) + host.Run(new RealmTestGame(async () => { - Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); - await testAction(realmFactory, testStorage); + var testStorage = storage.GetStorageForDirectory(caller); + + using (var realmFactory = new RealmContextFactory(testStorage, caller)) + { + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); + await testAction(realmFactory, testStorage); - realmFactory.Dispose(); + realmFactory.Dispose(); - Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); - realmFactory.Compact(); - Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); + Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); + realmFactory.Compact(); + } + })); + } + } + + protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset) + { + RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() }; + + var metadata = new RealmBeatmapMetadata + { + Title = "My Love", + Artist = "Kuba Oms" + }; + + var beatmapSet = new RealmBeatmapSet + { + Beatmaps = + { + new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", }, + new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", }, + new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", }, + new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", } + }, + Files = + { + new RealmNamedFileUsage(createRealmFile(), "test [easy].osu"), + new RealmNamedFileUsage(createRealmFile(), "test [normal].osu"), + new RealmNamedFileUsage(createRealmFile(), "test [hard].osu"), + new RealmNamedFileUsage(createRealmFile(), "test [insane].osu"), } - }); + }; + + for (int i = 0; i < 8; i++) + beatmapSet.Files.Add(new RealmNamedFileUsage(createRealmFile(), $"hitsound{i}.mp3")); + + foreach (var b in beatmapSet.Beatmaps) + b.BeatmapSet = beatmapSet; + + return beatmapSet; + } + + protected static RealmRuleset CreateRuleset() => + new RealmRuleset(0, "osu!", "osu", true); + + private class RealmTestGame : Framework.Game + { + public RealmTestGame(Func work) + { + // ReSharper disable once AsyncVoidLambda + Scheduler.Add(async () => + { + await work().ConfigureAwait(true); + Exit(); + }); + } + + public RealmTestGame(Action work) + { + Scheduler.Add(() => + { + work(); + Exit(); + }); + } } private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory) diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs new file mode 100644 index 000000000000..f4e0838be12a --- /dev/null +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Models; +using osu.Game.Stores; + +namespace osu.Game.Tests.Database +{ + public class RulesetStoreTests : RealmTest + { + [Test] + public void TestCreateStore() + { + RunTestWithRealm((realmFactory, storage) => + { + var rulesets = new RealmRulesetStore(realmFactory, storage); + + Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); + Assert.AreEqual(4, realmFactory.Context.All().Count()); + }); + } + + [Test] + public void TestCreateStoreTwiceDoesntAddRulesetsAgain() + { + RunTestWithRealm((realmFactory, storage) => + { + var rulesets = new RealmRulesetStore(realmFactory, storage); + var rulesets2 = new RealmRulesetStore(realmFactory, storage); + + Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); + Assert.AreEqual(4, rulesets2.AvailableRulesets.Count()); + + Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First()); + Assert.AreEqual(4, realmFactory.Context.All().Count()); + }); + } + + [Test] + public void TestRetrievedRulesetsAreDetached() + { + RunTestWithRealm((realmFactory, storage) => + { + var rulesets = new RealmRulesetStore(realmFactory, storage); + + Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false); + Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false); + Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false); + }); + } + } +} diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs new file mode 100644 index 000000000000..f3a4f102103e --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs @@ -0,0 +1,104 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Storyboards; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using FileInfo = osu.Game.IO.FileInfo; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckAudioInVideoTest + { + private CheckAudioInVideo check; + private IBeatmap beatmap; + + [SetUp] + public void Setup() + { + check = new CheckAudioInVideo(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = new List(new[] + { + new BeatmapSetFileInfo + { + Filename = "abc123.mp4", + FileInfo = new FileInfo { Hash = "abcdef" } + } + }) + } + } + }; + } + + [Test] + public void TestRegularVideoFile() + { + using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4")) + Assert.IsEmpty(check.Run(getContext(resourceStream))); + } + + [Test] + public void TestVideoFileWithAudio() + { + using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-audio.mp4")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack); + } + } + + [Test] + public void TestVideoFileWithTrackButNoAudio() + { + using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-track-but-no-audio.mp4")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack); + } + } + + [Test] + public void TestMissingFile() + { + beatmap.BeatmapInfo.BeatmapSet.Files.Clear(); + + var issues = check.Run(getContext(null)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateMissingFile); + } + + private BeatmapVerifierContext getContext(Stream resourceStream) + { + var storyboard = new Storyboard(); + var layer = storyboard.GetLayer("Video"); + layer.Add(new StoryboardVideo("abc123.mp4", 0)); + + var mockWorkingBeatmap = new Mock(beatmap, null, null); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); + mockWorkingBeatmap.As().SetupGet(w => w.Storyboard).Returns(storyboard); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs new file mode 100644 index 000000000000..9b090591bcde --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs @@ -0,0 +1,128 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ManagedBass; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Audio; +using FileInfo = osu.Game.IO.FileInfo; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckTooShortAudioFilesTest + { + private CheckTooShortAudioFiles check; + private IBeatmap beatmap; + + [SetUp] + public void Setup() + { + check = new CheckTooShortAudioFiles(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = new List(new[] + { + new BeatmapSetFileInfo + { + Filename = "abc123.wav", + FileInfo = new FileInfo { Hash = "abcdef" } + } + }) + } + } + }; + + // 0 = No output device. This still allows decoding. + if (!Bass.Init(0) && Bass.LastError != Errors.Already) + throw new AudioException("Could not initialize Bass."); + } + + [Test] + public void TestDifferentExtension() + { + beatmap.BeatmapInfo.BeatmapSet.Files.Clear(); + beatmap.BeatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo + { + Filename = "abc123.jpg", + FileInfo = new FileInfo { Hash = "abcdef" } + }); + + // Should fail to load, but not produce an error due to the extension not being expected to load. + Assert.IsEmpty(check.Run(getContext(null, allowMissing: true))); + } + + [Test] + public void TestRegularAudioFile() + { + using (var resourceStream = TestResources.OpenResource("Samples/test-sample.mp3")) + { + Assert.IsEmpty(check.Run(getContext(resourceStream))); + } + } + + [Test] + public void TestBlankAudioFile() + { + using (var resourceStream = TestResources.OpenResource("Samples/blank.wav")) + { + // This is a 0 ms duration audio file, commonly used to silence sliderslides/ticks, and so should be fine. + Assert.IsEmpty(check.Run(getContext(resourceStream))); + } + } + + [Test] + public void TestTooShortAudioFile() + { + using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateTooShort); + } + } + + [Test] + public void TestMissingAudioFile() + { + using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3")) + { + Assert.IsEmpty(check.Run(getContext(resourceStream, allowMissing: true))); + } + } + + [Test] + public void TestCorruptAudioFile() + { + using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat); + } + } + + private BeatmapVerifierContext getContext(Stream resourceStream, bool allowMissing = false) + { + var mockWorkingBeatmap = new Mock(beatmap, null, null); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs new file mode 100644 index 000000000000..c9adc030c1b0 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs @@ -0,0 +1,86 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using FileInfo = osu.Game.IO.FileInfo; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckZeroByteFilesTest + { + private CheckZeroByteFiles check; + private IBeatmap beatmap; + + [SetUp] + public void Setup() + { + check = new CheckZeroByteFiles(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = new List(new[] + { + new BeatmapSetFileInfo + { + Filename = "abc123.jpg", + FileInfo = new FileInfo { Hash = "abcdef" } + } + }) + } + } + }; + } + + [Test] + public void TestNonZeroBytes() + { + Assert.IsEmpty(check.Run(getContext(byteLength: 44))); + } + + [Test] + public void TestZeroBytes() + { + var issues = check.Run(getContext(byteLength: 0)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckZeroByteFiles.IssueTemplateZeroBytes); + } + + [Test] + public void TestMissing() + { + Assert.IsEmpty(check.Run(getContextMissing())); + } + + private BeatmapVerifierContext getContext(long byteLength) + { + var mockStream = new Mock(); + mockStream.Setup(s => s.Length).Returns(byteLength); + + var mockWorkingBeatmap = new Mock(); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(mockStream.Object); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + + private BeatmapVerifierContext getContextMissing() + { + var mockWorkingBeatmap = new Mock(); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns((Stream)null); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index bd34eaff6333..8eb9452736da 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -8,6 +8,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; @@ -53,10 +54,8 @@ public void Setup() => Schedule(() => BeatDivisor.Value = 1; - composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1; + composer.EditorBeatmap.Difficulty.SliderMultiplier = 1; composer.EditorBeatmap.ControlPointInfo.Clear(); - - composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 }); composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }); }); @@ -64,7 +63,7 @@ public void Setup() => Schedule(() => [TestCase(2)] public void TestSliderMultiplier(float multiplier) { - AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = multiplier); + AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier); assertSnapDistance(100 * multiplier); } @@ -73,13 +72,13 @@ public void TestSliderMultiplier(float multiplier) [TestCase(2)] public void TestSpeedMultiplier(float multiplier) { - AddStep($"set multiplier = {multiplier}", () => + assertSnapDistance(100 * multiplier, new HitObject { - composer.EditorBeatmap.ControlPointInfo.Clear(); - composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = multiplier }); + DifficultyControlPoint = new DifficultyControlPoint + { + SliderVelocity = multiplier + } }); - - assertSnapDistance(100 * multiplier); } [TestCase(1)] @@ -97,7 +96,7 @@ public void TestConvertDurationToDistance() assertDurationToDistance(500, 50); assertDurationToDistance(1000, 100); - AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2); assertDurationToDistance(500, 100); assertDurationToDistance(1000, 200); @@ -118,7 +117,7 @@ public void TestConvertDistanceToDuration() assertDistanceToDuration(50, 500); assertDistanceToDuration(100, 1000); - AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2); assertDistanceToDuration(100, 500); assertDistanceToDuration(200, 1000); @@ -143,7 +142,7 @@ public void TestGetSnappedDurationFromDistance() assertSnappedDuration(200, 2000); assertSnappedDuration(250, 3000); - AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2); assertSnappedDuration(0, 0); assertSnappedDuration(50, 0); @@ -175,7 +174,7 @@ public void GetSnappedDistanceFromDistance() assertSnappedDistance(200, 200); assertSnappedDistance(250, 200); - AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2); assertSnappedDistance(50, 0); assertSnappedDistance(100, 0); @@ -197,20 +196,20 @@ public void GetSnappedDistanceFromDistance() assertSnappedDistance(400, 400); } - private void assertSnapDistance(float expectedDistance) - => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(0) == expectedDistance); + private void assertSnapDistance(float expectedDistance, HitObject hitObject = null) + => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()) == expectedDistance); private void assertDurationToDistance(double duration, float expectedDistance) - => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(0, duration) == expectedDistance); + => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance); private void assertDistanceToDuration(float distance, double expectedDuration) - => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(0, distance) == expectedDuration); + => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration); private void assertSnappedDuration(float distance, double expectedDuration) - => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(0, distance) == expectedDuration); + => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(new HitObject(), distance) == expectedDuration); private void assertSnappedDistance(float distance, float expectedDistance) - => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(0, distance) == expectedDistance); + => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(new HitObject(), distance) == expectedDistance); private class TestHitObjectComposer : OsuHitObjectComposer { diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index de740dab61c1..c5a58af603e8 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -165,7 +165,7 @@ public void TestBonusObjectsExcludedFromDrain() { var beatmap = new Beatmap { - BeatmapInfo = { BaseDifficulty = { DrainRate = 10 } }, + Difficulty = { DrainRate = 10 } }; beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 0 }); @@ -200,7 +200,7 @@ private Beatmap createBeatmap(double startTime, double endTime, params BreakPeri { var beatmap = new Beatmap { - BeatmapInfo = { BaseDifficulty = { DrainRate = 10 } }, + Difficulty = { DrainRate = 10 } }; for (double time = startTime; time <= endTime; time += 100) diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index e888f51e9863..dbeb453d4dde 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -17,7 +17,7 @@ public abstract class ImportTest protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false) { var osu = new TestOsuGameBase(withBeatmap); - Task.Run(() => host.Run(osu)) + Task.Factory.StartNew(() => host.Run(osu), TaskCreationOptions.LongRunning) .ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted); waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index fabb016d5fb0..cfda4f6422f5 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -46,7 +46,7 @@ public void TestAddRedundantTiming() [Test] public void TestAddRedundantDifficulty() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); cpi.Add(0, new DifficultyControlPoint()); // is redundant cpi.Add(1000, new DifficultyControlPoint()); // is redundant @@ -55,7 +55,7 @@ public void TestAddRedundantDifficulty() Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0)); Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0)); - cpi.Add(1000, new DifficultyControlPoint { SpeedMultiplier = 2 }); // is not redundant + cpi.Add(1000, new DifficultyControlPoint { SliderVelocity = 2 }); // is not redundant Assert.That(cpi.Groups.Count, Is.EqualTo(1)); Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1)); @@ -159,7 +159,7 @@ public void TestRemoveGroupAlsoRemovedControlPoints() [Test] public void TestAddControlPointToGroup() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); var group = cpi.GroupAt(1000, true); Assert.That(cpi.Groups.Count, Is.EqualTo(1)); @@ -174,23 +174,23 @@ public void TestAddControlPointToGroup() [Test] public void TestAddDuplicateControlPointToGroup() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); var group = cpi.GroupAt(1000, true); Assert.That(cpi.Groups.Count, Is.EqualTo(1)); group.Add(new DifficultyControlPoint()); - group.Add(new DifficultyControlPoint { SpeedMultiplier = 2 }); + group.Add(new DifficultyControlPoint { SliderVelocity = 2 }); Assert.That(group.ControlPoints.Count, Is.EqualTo(1)); Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1)); - Assert.That(cpi.DifficultyPoints.First().SpeedMultiplier, Is.EqualTo(2)); + Assert.That(cpi.DifficultyPoints.First().SliderVelocity, Is.EqualTo(2)); } [Test] public void TestRemoveControlPointFromGroup() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); var group = cpi.GroupAt(1000, true); Assert.That(cpi.Groups.Count, Is.EqualTo(1)); @@ -208,14 +208,14 @@ public void TestRemoveControlPointFromGroup() [Test] public void TestOrdering() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); cpi.Add(0, new TimingControlPoint()); cpi.Add(1000, new TimingControlPoint { BeatLength = 500 }); cpi.Add(10000, new TimingControlPoint { BeatLength = 200 }); cpi.Add(5000, new TimingControlPoint { BeatLength = 100 }); - cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 }); - cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 }); + cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 }); + cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 }); cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 }); cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true }); @@ -230,14 +230,14 @@ public void TestOrdering() [Test] public void TestClear() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); cpi.Add(0, new TimingControlPoint()); cpi.Add(1000, new TimingControlPoint { BeatLength = 500 }); cpi.Add(10000, new TimingControlPoint { BeatLength = 200 }); cpi.Add(5000, new TimingControlPoint { BeatLength = 100 }); - cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 }); - cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 }); + cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 }); + cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 }); cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 }); cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true }); diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 5f76bf8bd6fe..978767c265c1 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -168,14 +168,14 @@ protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host); } - protected override BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host) + protected override BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager manager, IAPIProvider api, GameHost host) { - return new TestBeatmapModelDownloader(modelManager, api, host); + return new TestBeatmapModelDownloader(manager, api, host); } internal class TestBeatmapModelDownloader : BeatmapModelDownloader { - public TestBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost) + public TestBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost) : base(modelManager, apiProvider, gameHost) { } diff --git a/osu.Game.Tests/Resources/Samples/blank.wav b/osu.Game.Tests/Resources/Samples/blank.wav new file mode 100644 index 000000000000..878bf23ceab9 Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/blank.wav differ diff --git a/osu.Game.Tests/Resources/Samples/corrupt.wav b/osu.Game.Tests/Resources/Samples/corrupt.wav new file mode 100644 index 000000000000..87c7de4b7beb Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/corrupt.wav differ diff --git a/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 new file mode 100644 index 000000000000..003fe23dca22 Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 differ diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 new file mode 100644 index 000000000000..5d380ab50cc0 Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 differ diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 new file mode 100644 index 000000000000..7cdd1939e95f Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 differ diff --git a/osu.Game.Tests/Resources/Videos/test-video.mp4 b/osu.Game.Tests/Resources/Videos/test-video.mp4 new file mode 100644 index 000000000000..795483c096cf Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video.mp4 differ diff --git a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs index ab470674117a..ffb3d41d188d 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; @@ -65,10 +64,9 @@ public TestSkinProvidingContainer(IEnumerable sources) public new void TriggerSourceChanged() => base.TriggerSourceChanged(); - protected override void OnSourceChanged() + protected override void RefreshSources() { - ResetSources(); - sources.ForEach(AddSource); + SetSources(sources); } } diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs new file mode 100644 index 000000000000..99be72e95865 --- /dev/null +++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs @@ -0,0 +1,173 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using ManagedBass.Fx; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Game.Audio.Effects; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Tests.Visual.Audio +{ + public class TestSceneAudioFilter : OsuTestScene + { + private OsuSpriteText lowPassText; + private AudioFilter lowPassFilter; + + private OsuSpriteText highPassText; + private AudioFilter highPassFilter; + + private Track track; + + private WaveformTestBeatmap beatmap; + + private OsuSliderBar lowPassSlider; + private OsuSliderBar highPassSlider; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + beatmap = new WaveformTestBeatmap(audio); + track = beatmap.LoadTrack(); + + Add(new FillFlowContainer + { + Children = new Drawable[] + { + lowPassFilter = new AudioFilter(audio.TrackMixer), + highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), + lowPassText = new OsuSpriteText + { + Padding = new MarginPadding(20), + Text = $"Low Pass: {lowPassFilter.Cutoff}hz", + Font = new FontUsage(size: 40) + }, + lowPassSlider = new OsuSliderBar + { + Width = 500, + Height = 50, + Padding = new MarginPadding(20), + Current = new BindableInt + { + MinValue = 0, + MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF, + } + }, + highPassText = new OsuSpriteText + { + Padding = new MarginPadding(20), + Text = $"High Pass: {highPassFilter.Cutoff}hz", + Font = new FontUsage(size: 40) + }, + highPassSlider = new OsuSliderBar + { + Width = 500, + Height = 50, + Padding = new MarginPadding(20), + Current = new BindableInt + { + MinValue = 0, + MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF, + } + } + } + }); + + lowPassSlider.Current.ValueChanged += e => + { + lowPassText.Text = $"Low Pass: {e.NewValue}hz"; + lowPassFilter.Cutoff = e.NewValue; + }; + + highPassSlider.Current.ValueChanged += e => + { + highPassText.Text = $"High Pass: {e.NewValue}hz"; + highPassFilter.Cutoff = e.NewValue; + }; + } + + #region Overrides of Drawable + + protected override void Update() + { + base.Update(); + highPassSlider.Current.Value = highPassFilter.Cutoff; + lowPassSlider.Current.Value = lowPassFilter.Cutoff; + } + + #endregion + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Play Track", () => track.Start()); + + AddStep("Reset filters", () => + { + lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF; + highPassFilter.Cutoff = 0; + }); + + waitTrackPlay(); + } + + [Test] + public void TestLowPassSweep() + { + AddStep("Filter Sweep", () => + { + lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() + .CutoffTo(0, 2000, Easing.OutCubic); + }); + + waitTrackPlay(); + + AddStep("Filter Sweep (reverse)", () => + { + lowPassFilter.CutoffTo(0).Then() + .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); + }); + + waitTrackPlay(); + AddStep("Stop track", () => track.Stop()); + } + + [Test] + public void TestHighPassSweep() + { + AddStep("Filter Sweep", () => + { + highPassFilter.CutoffTo(0).Then() + .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); + }); + + waitTrackPlay(); + + AddStep("Filter Sweep (reverse)", () => + { + highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() + .CutoffTo(0, 2000, Easing.OutCubic); + }); + + waitTrackPlay(); + + AddStep("Stop track", () => track.Stop()); + } + + private void waitTrackPlay() => AddWaitStep("Let track play", 10); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + track?.Dispose(); + } + } +} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 11830ebe35a2..d1efd22d6f40 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; @@ -81,7 +82,7 @@ private class TestDistanceSnapGrid : DistanceSnapGrid public new float DistanceSpacing => base.DistanceSpacing; public TestDistanceSnapGrid(double? endTime = null) - : base(grid_position, 0, endTime) + : base(new HitObject(), grid_position, 0, endTime) { } @@ -158,15 +159,15 @@ public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosi public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); - public float GetBeatSnapDistanceAt(double referenceTime) => 10; + public float GetBeatSnapDistanceAt(HitObject referenceObject) => 10; - public float DurationToDistance(double referenceTime, double duration) => (float)duration; + public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration; - public double DistanceToDuration(double referenceTime, float distance) => distance; + public double DistanceToDuration(HitObject referenceObject, float distance) => distance; - public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0; + public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0; - public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0; + public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0; } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 2258a209e207..ab2bc4649a41 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; using osuTK.Input; @@ -30,22 +31,36 @@ public void TestNewBeatmapSaveThenLoad() PushAndConfirm(() => new EditorLoader()); - AddUntilStep("wait for editor load", () => editor != null); + AddUntilStep("wait for editor load", () => editor?.IsLoaded == true); - AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + + // We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten. AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7); + AddStep("Set artist and title", () => + { + editorBeatmap.BeatmapInfo.Metadata.Artist = "artist"; + editorBeatmap.BeatmapInfo.Metadata.Title = "title"; + }); + AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.Version = "difficulty"); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + AddStep("Change to placement mode", () => InputManager.Key(Key.Number2)); AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left)); - AddStep("Save and exit", () => - { - InputManager.Keys(PlatformAction.Save); - InputManager.Key(Key.Escape); - }); + checkMutations(); + + AddStep("Save", () => InputManager.Keys(PlatformAction.Save)); + + checkMutations(); + + AddStep("Exit", () => InputManager.Key(Key.Escape)); AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); @@ -56,7 +71,16 @@ public void TestNewBeatmapSaveThenLoad() AddStep("Enter editor", () => InputManager.Key(Key.Number5)); AddUntilStep("Wait for editor load", () => editor != null); + + checkMutations(); + } + + private void checkMutations() + { AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); + AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7); + AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title"); + AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.Version == "difficulty"); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index 5abb1fb011cc..3a7fdb1e7b24 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -182,7 +182,7 @@ public void TestSliderMultiplierDoesNotAffectRelativeBeatLength() { var beatmap = createBeatmap(); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); - beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2; + beatmap.Difficulty.SliderMultiplier = 2; createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true); AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 5000); @@ -196,7 +196,7 @@ public void TestSliderMultiplierAffectsNonRelativeBeatLength() { var beatmap = createBeatmap(); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); - beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2; + beatmap.Difficulty.SliderMultiplier = 2; createTest(beatmap); AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 2000); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs index 5eb71e92c2d9..ae0decaee1b0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs @@ -103,6 +103,30 @@ public void TestInitialSeek() checkFrameCount(0); } + [Test] + public void TestRatePreservedWhenTimeNotProgressing() + { + AddStep("set manual clock rate", () => manualClock.Rate = 1); + seekManualTo(5000); + createStabilityContainer(); + checkRate(1); + + seekManualTo(10000); + checkRate(1); + + AddWaitStep("wait some", 3); + checkRate(1); + + seekManualTo(5000); + checkRate(-1); + + AddWaitStep("wait some", 3); + checkRate(-1); + + seekManualTo(10000); + checkRate(1); + } + private const int max_frames_catchup = 50; private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () => @@ -116,6 +140,9 @@ private void createStabilityContainer(double gameplayStartTime = double.MinValue private void checkFrameCount(int frames) => AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames); + private void checkRate(double rate) => + AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate); + public class ClockConsumingChild : CompositeDrawable { private readonly OsuSpriteText text; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 6e7c9c6bef01..b9fd35bba97a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -55,7 +55,7 @@ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap { - BeatmapInfo = { BaseDifficulty = { ApproachRate = 9 } }, + Difficulty = { ApproachRate = 9 }, }; for (int i = 0; i < 15; i++) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 1dcd37544021..57d2e6c828ba 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -205,7 +205,7 @@ public void TestModReinstantiation() AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen()); - AddStep("retrieve mods", () => playerMod1 = (TestMod)player.Mods.Value.Single()); + AddStep("retrieve mods", () => playerMod1 = (TestMod)player.GameplayState.Mods.Single()); AddAssert("game mods not applied", () => gameMod.Applied == false); AddAssert("player mods applied", () => playerMod1.Applied); @@ -217,7 +217,7 @@ public void TestModReinstantiation() }); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen()); - AddStep("retrieve mods", () => playerMod2 = (TestMod)player.Mods.Value.Single()); + AddStep("retrieve mods", () => playerMod2 = (TestMod)player.GameplayState.Mods.Single()); AddAssert("game mods not applied", () => gameMod.Applied == false); AddAssert("player has different mods", () => playerMod1 != playerMod2); AddAssert("player mods applied", () => playerMod2.Applied); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index 2f15e549f774..283fe594eabf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -93,9 +93,9 @@ private void setUpHitObjects() => AddStep("set up hit objects", () => private IList testControlPoints => new List { - new MultiplierControlPoint(time_range) { DifficultyPoint = { SpeedMultiplier = 1.25 } }, - new MultiplierControlPoint(1.5 * time_range) { DifficultyPoint = { SpeedMultiplier = 1 } }, - new MultiplierControlPoint(2 * time_range) { DifficultyPoint = { SpeedMultiplier = 1.5 } } + new MultiplierControlPoint(time_range) { EffectPoint = { ScrollSpeed = 1.25 } }, + new MultiplierControlPoint(1.5 * time_range) { EffectPoint = { ScrollSpeed = 1 } }, + new MultiplierControlPoint(2 * time_range) { EffectPoint = { ScrollSpeed = 1.5 } } }; [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 8fa842ec8133..4cd40d7c5336 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -90,8 +90,12 @@ public void TestOutroEndsDuringFailAnimation() CreateTest(() => { AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true); - AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300); + + // Fail occurs at 164ms with the provided beatmap. + // Fail animation runs for 2.5s realtime but the gameplay time change is *variable* due to the frequency transform being applied, so we need a bit of lenience. + AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600); }); + AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index f71d13ed3534..bfea97410ad5 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Framework.Screens; using osu.Game.Screens; using osu.Game.Screens.Menu; using osuTK; @@ -21,6 +20,8 @@ public abstract class IntroTestScene : OsuTestScene protected OsuScreenStack IntroStack; + private IntroScreen intro; + protected IntroTestScene() { Children = new Drawable[] @@ -39,7 +40,11 @@ protected IntroTestScene() Position = new Vector2(0.5f), } }; + } + [Test] + public virtual void TestPlayIntro() + { AddStep("restart sequence", () => { logo.FinishTransforms(); @@ -52,12 +57,12 @@ protected IntroTestScene() RelativeSizeAxes = Axes.Both, }); - IntroStack.Push(CreateScreen()); + IntroStack.Push(intro = CreateScreen()); }); - AddUntilStep("wait for menu", () => IntroStack.CurrentScreen is MainMenu); + AddUntilStep("wait for menu", () => intro.DidLoadMenu); } - protected abstract IScreen CreateScreen(); + protected abstract IntroScreen CreateScreen(); } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs index 107734cc8d4f..ffc99185fbf5 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Screens; using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus @@ -10,6 +9,6 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneIntroCircles : IntroTestScene { - protected override IScreen CreateScreen() => new IntroCircles(); + protected override IntroScreen CreateScreen() => new IntroCircles(); } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs index df79584167ca..8f01e0321b9d 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Screens; using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus @@ -10,6 +9,6 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneIntroTriangles : IntroTestScene { - protected override IScreen CreateScreen() => new IntroTriangles(); + protected override IntroScreen CreateScreen() => new IntroTriangles(); } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 5f135febf47a..9081be3dd653 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Screens.Menu; @@ -11,10 +10,12 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneIntroWelcome : IntroTestScene { - protected override IScreen CreateScreen() => new IntroWelcome(); + protected override IntroScreen CreateScreen() => new IntroWelcome(); - public TestSceneIntroWelcome() + public override void TestPlayIntro() { + base.TestPlayIntro(); + AddUntilStep("wait for load", () => MusicController.TrackLoaded); AddAssert("correct track", () => Precision.AlmostEquals(MusicController.CurrentTrack.Length, 48000, 1)); AddAssert("check if menu music loops", () => MusicController.CurrentTrack.Looping); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 0b707038700a..2bb77395eff0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -564,11 +564,18 @@ public void TestGameplayFlow() } }); - AddRepeatStep("click spectate button", () => + AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value); + + AddStep("click ready button", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.MoveMouseTo(readyButton); InputManager.Click(MouseButton.Left); - }, 2); + }); + + AddUntilStep("wait for player to be ready", () => client.Room?.Users[0].State == MultiplayerUserState.Ready); + AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value); + + AddStep("click start button", () => InputManager.Click(MouseButton.Left)); AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); @@ -582,6 +589,8 @@ public void TestGameplayFlow() AddUntilStep("wait for results", () => Stack.CurrentScreen is ResultsScreen); } + private MultiplayerReadyButton readyButton => this.ChildrenOfType().Single(); + private void createRoom(Func room) { AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); diff --git a/osu.Game.Tests/Visual/Mvis/TestSceneTextEditIndicator.cs b/osu.Game.Tests/Visual/Mvis/TestSceneTextEditIndicator.cs deleted file mode 100644 index 08a4eb280bd0..000000000000 --- a/osu.Game.Tests/Visual/Mvis/TestSceneTextEditIndicator.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Utils; -using osu.Game.Graphics; - -namespace osu.Game.Tests.Visual.Mvis -{ - public class TestSceneTextEditIndicator : ScreenTestScene - { - private DependencyContainer dependencies; - private TextEditIndicator indicator; - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => - dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - - [BackgroundDependencyLoader] - private void load() - { - Children = new Drawable[] - { - new Box - { - Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#777"), Color4Extensions.FromHex("#aaa")), - RelativeSizeAxes = Axes.Both - }, - indicator = new TextEditIndicator() - }; - - AddStep("Toggle Indicator", indicator.ToggleVisibility); - AddStep("Clear Indicator", () => indicator.Text = string.Empty); - AddStep("Random Text", () => - { - indicator.Text += RNG.Next().ToString(); - indicator.Show(); - }); - AddStep("Flash", indicator.Flash); - AddStep("Test", () => - { - indicator.Text = "测试"; - indicator.Text = string.Empty; - this.Delay(3000).Schedule(() => indicator.Text = "Hey!"); - }); - } - } -} diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index 7327d4053a06..2706ff5cebf7 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -7,11 +7,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.Platform; -using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -29,12 +26,11 @@ using osu.Game.Screens.Menu; using osu.Game.Skinning; using osu.Game.Utils; -using osuTK.Graphics; namespace osu.Game.Tests.Visual.Navigation { [TestFixture] - public class TestSceneOsuGame : OsuTestScene + public class TestSceneOsuGame : OsuGameTestScene { private IReadOnlyList requiredGameDependencies => new[] { @@ -84,34 +80,12 @@ public class TestSceneOsuGame : OsuTestScene typeof(PreviewTrackManager), }; - private OsuGame game; - [Resolved] private OsuGameBase gameBase { get; set; } [Resolved] private GameHost host { get; set; } - [SetUpSteps] - public void SetUpSteps() - { - AddStep("create game", () => - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - }; - - AddGame(game = new OsuGame()); - }); - - AddUntilStep("wait for load", () => game.IsLoaded); - } - [Test] public void TestNullRulesetHandled() { @@ -127,8 +101,8 @@ public void TestNullRulesetHandled() [Test] public void TestSwitchThreadExecutionMode() { - AddStep("Change thread mode to multi threaded", () => { game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.MultiThreaded); }); - AddStep("Change thread mode to single thread", () => { game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.SingleThread); }); + AddStep("Change thread mode to multi threaded", () => { Game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.MultiThreaded); }); + AddStep("Change thread mode to single thread", () => { Game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.SingleThread); }); } [Test] @@ -154,7 +128,7 @@ public void TestAvailableDependencies() { foreach (var type in requiredGameDependencies) { - if (game.Dependencies.Get(type) == null) + if (Game.Dependencies.Get(type) == null) throw new InvalidOperationException($"{type} has not been cached"); } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index aeb800f58a53..ce437e729958 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -350,13 +350,13 @@ public void TestOverlayClosing() // since most overlays use a scroll container that absorbs on mouse down NowPlayingOverlay nowPlayingOverlay = null; + AddUntilStep("Wait for now playing load", () => (nowPlayingOverlay = Game.ChildrenOfType().FirstOrDefault()) != null); + AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("toolbar displayed", () => Game.Toolbar.State.Value == Visibility.Visible); - AddStep("get and press now playing hotkey", () => - { - nowPlayingOverlay = Game.ChildrenOfType().Single(); - InputManager.Key(Key.F6); - }); + AddStep("open now playing", () => InputManager.Key(Key.F6)); + AddUntilStep("now playing is visible", () => nowPlayingOverlay.State.Value == Visibility.Visible); // drag tests @@ -417,7 +417,7 @@ public void TestExitGameFromSongSelect() pushEscape(); // returns to osu! logo AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); - AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroTriangles); + AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroScreen); AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null); AddStep("test dispose doesn't crash", () => Game.Dispose()); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs new file mode 100644 index 000000000000..bd723eeed650 --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Overlays.Notifications; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneStartupImport : OsuGameTestScene + { + private string importFilename; + + protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { importFilename }); + + public override void SetUpSteps() + { + AddStep("Prepare import beatmap", () => importFilename = TestResources.GetTestBeatmapForImport()); + + base.SetUpSteps(); + } + + [Test] + public void TestImportCreatedNotification() + { + AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType().Count() == 1); + } + } +} diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 4bcc887b9f95..d948aebbbf30 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -32,12 +32,14 @@ public class TestScenePlaylistsResultsScreen : ScreenTestScene private TestResultsScreen resultsScreen; private int currentScoreId; private bool requestComplete; + private int totalCount; [SetUp] public void Setup() => Schedule(() => { currentScoreId = 0; requestComplete = false; + totalCount = 0; bindHandler(); }); @@ -53,7 +55,6 @@ public void TestShowWithUserScore() }); createResults(() => userScore); - waitForDisplay(); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded); } @@ -62,7 +63,6 @@ public void TestShowWithUserScore() public void TestShowNullUserScore() { createResults(); - waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } @@ -79,7 +79,6 @@ public void TestShowUserScoreWithDelay() }); createResults(() => userScore); - waitForDisplay(); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded); @@ -91,7 +90,6 @@ public void TestShowNullUserScoreWithDelay() AddStep("bind delayed handler", () => bindHandler(true)); createResults(); - waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } @@ -100,7 +98,6 @@ public void TestShowNullUserScoreWithDelay() public void TestFetchWhenScrolledToTheRight() { createResults(); - waitForDisplay(); AddStep("bind delayed handler", () => bindHandler(true)); @@ -131,7 +128,6 @@ public void TestFetchWhenScrolledToTheLeft() }); createResults(() => userScore); - waitForDisplay(); AddStep("bind delayed handler", () => bindHandler(true)); @@ -161,13 +157,15 @@ private void createResults(Func getScore = null) })); }); - AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true); + waitForDisplay(); } private void waitForDisplay() { - AddUntilStep("wait for request to complete", () => requestComplete); - AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true); + AddUntilStep("wait for load to complete", () => + requestComplete + && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount + && resultsScreen.ScorePanelList.AllPanelsVisible); AddWaitStep("wait for display", 5); } @@ -203,6 +201,7 @@ private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool triggerFail(s); else triggerSuccess(s, createUserResponse(userScore)); + break; case IndexPlaylistScoresRequest i: @@ -248,6 +247,8 @@ private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore) } }; + totalCount++; + for (int i = 1; i <= scores_per_result; i++) { multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore @@ -285,6 +286,8 @@ private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore) }, Statistics = userScore.Statistics }); + + totalCount += 2; } addCursor(multiplayerUserScore.ScoresAround.Lower); @@ -325,6 +328,8 @@ private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest { HitResult.Great, 300 } } }); + + totalCount++; } addCursor(result); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index d8ec89a94e8e..deb9a22184b7 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -137,11 +137,11 @@ public void TestBeatmapUpdatedOnReImport() InputManager.Click(MouseButton.Left); }); - AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize == 1); + AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize == 1); AddStep("re-import original beatmap", () => manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait()); - AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize != 1); + AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize != 1); } private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs index e3dae9c27eeb..d530e1f796d1 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs @@ -53,7 +53,11 @@ private class TestTargetClass }; [SettingSource("Sample string", "Change something for a mod")] - public Bindable StringBindable { get; } = new Bindable(); + public Bindable StringBindable { get; } = new Bindable + { + Default = string.Empty, + Value = "Sample text" + }; } private enum TestEnum diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 997eac709d2d..dc5b0e0d7731 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Handlers.Tablet; @@ -21,6 +22,9 @@ public class TestSceneTabletSettings : OsuTestScene private TestTabletHandler tabletHandler; private TabletSettings settings; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + [SetUpSteps] public void SetUpSteps() { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index a5b90e665500..0ae4e0c5dc55 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -205,7 +205,7 @@ private void assertCollectionHeaderDisplays(string collectionName, bool shouldDi private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 - () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName))); + () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName))); private IconButton getAddOrRemoveButton(int index) => getCollectionDropdownItems().ElementAt(index).ChildrenOfType().Single(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 319ce5241d10..08420ec78cd9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -142,6 +142,8 @@ public void TestChangeBeatmapViaMouseBeforeEnter() AddStep("store selected beatmap", () => selected = Beatmap.Value); + AddUntilStep("wait for beatmaps to load", () => songSelect.Carousel.ChildrenOfType().Any()); + AddStep("select next and enter", () => { InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType() @@ -599,10 +601,10 @@ public void TestDifficultyIconSelecting() }); FilterableDifficultyIcon difficultyIcon = null; - AddStep("Find an icon", () => + AddUntilStep("Find an icon", () => { - difficultyIcon = set.ChildrenOfType() - .First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex()); + return (difficultyIcon = set.ChildrenOfType() + .FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex())) != null; }); AddStep("Click on a difficulty", () => @@ -765,10 +767,10 @@ public void TestGroupedDifficultyIconSelecting() }); FilterableGroupedDifficultyIcon groupIcon = null; - AddStep("Find group icon for different ruleset", () => + AddUntilStep("Find group icon for different ruleset", () => { - groupIcon = set.ChildrenOfType() - .First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3); + return (groupIcon = set.ChildrenOfType() + .FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3)) != null; }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index daae6ec0a265..1787d6c28a7e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -163,7 +163,6 @@ public void TestDeleteViaRightClick() }); AddUntilStep("wait for fetch", () => leaderboard.Scores != null); - AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID)); } @@ -171,6 +170,7 @@ public void TestDeleteViaRightClick() public void TestDeleteViaDatabase() { AddStep("delete top score", () => scoreManager.Delete(importedScores[0])); + AddUntilStep("wait for fetch", () => leaderboard.Scores != null); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID)); } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 546e905dede9..8d1572769ff9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -36,9 +36,9 @@ public void TestIncrementMultiplier() AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod)); AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod)); - var mutlipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; - AddStep(@"Add multiple Mods", () => changeMods(mutlipleIncrementMods)); - AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(mutlipleIncrementMods)); + var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; + AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods)); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs index 393420e700c7..1b7f65f9a035 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs @@ -1,11 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; namespace osu.Game.Tests.Visual.UserInterface { @@ -19,28 +23,62 @@ private void createSliderBar(bool hasDescription = false) { AddStep("create component", () => { - LabelledSliderBar component; + FillFlowContainer flow; - Child = new Container + Child = flow = new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 500, AutoSizeAxes = Axes.Y, - Child = component = new LabelledSliderBar + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Current = new BindableDouble(5) + new LabelledSliderBar { - MinValue = 0, - MaxValue = 10, - Precision = 1, - } - } + Current = new BindableDouble(5) + { + MinValue = 0, + MaxValue = 10, + Precision = 1, + }, + Label = "a sample component", + Description = hasDescription ? "this text describes the component" : string.Empty, + }, + }, }; - component.Label = "a sample component"; - component.Description = hasDescription ? "this text describes the component" : string.Empty; + foreach (var colour in Enum.GetValues(typeof(OverlayColourScheme)).OfType()) + { + flow.Add(new OverlayColourContainer(colour) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new LabelledSliderBar + { + Current = new BindableDouble(5) + { + MinValue = 0, + MaxValue = 10, + Precision = 1, + }, + Label = "a sample component", + Description = hasDescription ? "this text describes the component" : string.Empty, + } + }); + } }); } + + private class OverlayColourContainer : Container + { + [Cached] + private OverlayColourProvider colourProvider; + + public OverlayColourContainer(OverlayColourScheme scheme) + { + colourProvider = new OverlayColourProvider(scheme); + } + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs new file mode 100644 index 000000000000..9ccfba7c7430 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneRoundedButton : OsuTestScene + { + [Test] + public void TestBasic() + { + RoundedButton button = null; + + AddStep("create button", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.DarkGray + }, + button = new RoundedButton + { + Width = 400, + Text = "Test button", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => { } + } + } + }); + + AddToggleStep("toggle disabled", disabled => button.Action = disabled ? (Action)null : () => { }); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs new file mode 100644 index 000000000000..fb04c5bad093 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneSettingsCheckbox : OsuTestScene + { + [TestCase] + public void TestCheckbox() + { + AddStep("create component", () => + { + FillFlowContainer flow; + + Child = flow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = "a sample component", + }, + }, + }; + + foreach (var colour1 in Enum.GetValues(typeof(OverlayColourScheme)).OfType()) + { + flow.Add(new OverlayColourContainer(colour1) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new SettingsCheckbox + { + LabelText = "a sample component", + } + }); + } + }); + } + + private class OverlayColourContainer : Container + { + [Cached] + private OverlayColourProvider colourProvider; + + public OverlayColourContainer(OverlayColourScheme scheme) + { + colourProvider = new OverlayColourProvider(scheme); + } + } + } +} diff --git a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs index b14684200f6b..319a768e6590 100644 --- a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs @@ -14,7 +14,7 @@ public abstract class TournamentHostTest public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null) { tournament ??= new TournamentGameBase(); - Task.Run(() => host.Run(tournament)) + Task.Factory.StartNew(() => host.Run(tournament), TaskCreationOptions.LongRunning) .ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted); WaitForOrAssert(() => tournament.IsLoaded, @"osu! failed to start in a reasonable amount of time"); return tournament; diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index 5782301a6524..2237e389d705 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -26,7 +26,7 @@ public class DateTextBox : SettingsTextBox public DateTextBox() { - base.Current = new Bindable(); + base.Current = new Bindable(string.Empty); ((OsuTextBox)Control).OnCommit += (sender, newText) => { diff --git a/osu.Game.Tournament/Models/SeedingResult.cs b/osu.Game.Tournament/Models/SeedingResult.cs index 87aaf8bf3659..d37c967762ac 100644 --- a/osu.Game.Tournament/Models/SeedingResult.cs +++ b/osu.Game.Tournament/Models/SeedingResult.cs @@ -10,7 +10,7 @@ public class SeedingResult { public List Beatmaps = new List(); - public Bindable Mod = new Bindable(); + public Bindable Mod = new Bindable(string.Empty); public Bindable Seed = new BindableInt { diff --git a/osu.Game.Tournament/Models/TournamentRound.cs b/osu.Game.Tournament/Models/TournamentRound.cs index 08b3143be160..ab39605d0748 100644 --- a/osu.Game.Tournament/Models/TournamentRound.cs +++ b/osu.Game.Tournament/Models/TournamentRound.cs @@ -14,8 +14,8 @@ namespace osu.Game.Tournament.Models [Serializable] public class TournamentRound { - public readonly Bindable Name = new Bindable(); - public readonly Bindable Description = new Bindable(); + public readonly Bindable Name = new Bindable(string.Empty); + public readonly Bindable Description = new Bindable(string.Empty); public readonly BindableInt BestOf = new BindableInt(9) { Default = 9, MinValue = 3, MaxValue = 23 }; diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index c757d5e27e16..dc99a0586164 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -149,7 +149,7 @@ public class RoundBeatmapRow : CompositeDrawable private readonly Bindable beatmapId = new Bindable(); - private readonly Bindable mods = new Bindable(); + private readonly Bindable mods = new Bindable(string.Empty); private readonly Container drawableContainer; diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index a51348b35779..6eff554c7ca3 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -149,7 +149,7 @@ public class SeedingBeatmapRow : CompositeDrawable private readonly Bindable beatmapId = new Bindable(); - private readonly Bindable score = new Bindable(); + private readonly Bindable score = new Bindable(string.Empty); private readonly Container drawableContainer; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs index ab53132fd29a..2d5a20e10ec7 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs @@ -127,7 +127,7 @@ protected override void UpdateAfterChildren() score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth); } - private class MatchScoreCounter : ScoreCounter + private class MatchScoreCounter : CommaSeparatedScoreCounter { private OsuSpriteText displayedSpriteText; diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs new file mode 100644 index 000000000000..d2a39e9db7fe --- /dev/null +++ b/osu.Game/Audio/Effects/AudioFilter.cs @@ -0,0 +1,145 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using ManagedBass.Fx; +using osu.Framework.Audio.Mixing; +using osu.Framework.Graphics; + +namespace osu.Game.Audio.Effects +{ + public class AudioFilter : Component, ITransformableFilter + { + /// + /// The maximum cutoff frequency that can be used with a low-pass filter. + /// This is equal to nyquist - 1hz. + /// + public const int MAX_LOWPASS_CUTOFF = 22049; // nyquist - 1hz + + private readonly AudioMixer mixer; + private readonly BQFParameters filter; + private readonly BQFType type; + + private bool isAttached; + + private int cutoff; + + /// + /// The cutoff frequency of this filter. + /// + public int Cutoff + { + get => cutoff; + set + { + if (value == cutoff) + return; + + cutoff = value; + updateFilter(cutoff); + } + } + + /// + /// A Component that implements a BASS FX BiQuad Filter Effect. + /// + /// The mixer this effect should be applied to. + /// The type of filter (e.g. LowPass, HighPass, etc) + public AudioFilter(AudioMixer mixer, BQFType type = BQFType.LowPass) + { + this.mixer = mixer; + this.type = type; + + filter = new BQFParameters + { + lFilter = type, + fBandwidth = 0, + // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0) + fQ = 0.7f + }; + + Cutoff = getInitialCutoff(type); + } + + private int getInitialCutoff(BQFType type) + { + switch (type) + { + case BQFType.HighPass: + return 1; + + case BQFType.LowPass: + return MAX_LOWPASS_CUTOFF; + + default: + return 500; // A default that should ensure audio remains audible for other filters. + } + } + + private void updateFilter(int newValue) + { + switch (type) + { + case BQFType.LowPass: + // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz. + if (newValue >= MAX_LOWPASS_CUTOFF) + { + ensureDetached(); + return; + } + + break; + + // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz. + case BQFType.HighPass: + if (newValue <= 1) + { + ensureDetached(); + return; + } + + break; + } + + ensureAttached(); + + var filterIndex = mixer.Effects.IndexOf(filter); + + if (filterIndex < 0) return; + + if (mixer.Effects[filterIndex] is BQFParameters existingFilter) + { + existingFilter.fCenter = newValue; + + // required to update effect with new parameters. + mixer.Effects[filterIndex] = existingFilter; + } + } + + private void ensureAttached() + { + if (isAttached) + return; + + Debug.Assert(!mixer.Effects.Contains(filter)); + mixer.Effects.Add(filter); + isAttached = true; + } + + private void ensureDetached() + { + if (!isAttached) + return; + + Debug.Assert(mixer.Effects.Contains(filter)); + mixer.Effects.Remove(filter); + isAttached = false; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + ensureDetached(); + } + } +} diff --git a/osu.Game/Audio/Effects/ITransformableFilter.cs b/osu.Game/Audio/Effects/ITransformableFilter.cs new file mode 100644 index 000000000000..fb6a924f6829 --- /dev/null +++ b/osu.Game/Audio/Effects/ITransformableFilter.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; + +namespace osu.Game.Audio.Effects +{ + public interface ITransformableFilter + { + /// + /// The filter cutoff. + /// + int Cutoff { get; set; } + } + + public static class FilterableAudioComponentExtensions + { + /// + /// Smoothly adjusts filter cutoff over time. + /// + /// A to which further transforms can be added. + public static TransformSequence CutoffTo(this T component, int newCutoff, double duration = 0, Easing easing = Easing.None) + where T : class, ITransformableFilter, IDrawable + => component.CutoffTo(newCutoff, duration, new DefaultEasingFunction(easing)); + + /// + /// Smoothly adjusts filter cutoff over time. + /// + /// A to which further transforms can be added. + public static TransformSequence CutoffTo(this TransformSequence sequence, int newCutoff, double duration = 0, Easing easing = Easing.None) + where T : class, ITransformableFilter, IDrawable + => sequence.CutoffTo(newCutoff, duration, new DefaultEasingFunction(easing)); + + /// + /// Smoothly adjusts filter cutoff over time. + /// + /// A to which further transforms can be added. + public static TransformSequence CutoffTo(this T component, int newCutoff, double duration, TEasing easing) + where T : class, ITransformableFilter, IDrawable + where TEasing : IEasingFunction + => component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing); + + /// + /// Smoothly adjusts filter cutoff over time. + /// + /// A to which further transforms can be added. + public static TransformSequence CutoffTo(this TransformSequence sequence, int newCutoff, double duration, TEasing easing) + where T : class, ITransformableFilter, IDrawable + where TEasing : IEasingFunction + => sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing)); + } +} diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index e5b6a4bc44f2..b2211e26cfff 100755 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -18,17 +18,48 @@ namespace osu.Game.Beatmaps public class Beatmap : IBeatmap where T : HitObject { - public BeatmapInfo BeatmapInfo { get; set; } = new BeatmapInfo + private BeatmapDifficulty difficulty = new BeatmapDifficulty(); + + public BeatmapDifficulty Difficulty + { + get => difficulty; + set + { + difficulty = value; + + if (beatmapInfo != null) + beatmapInfo.BaseDifficulty = difficulty.Clone(); + } + } + + private BeatmapInfo beatmapInfo; + + public BeatmapInfo BeatmapInfo { - Metadata = new BeatmapMetadata + get => beatmapInfo; + set { - Artist = @"Unknown", - Title = @"Unknown", - AuthorString = @"Unknown Creator", - }, - Version = @"Normal", - BaseDifficulty = new BeatmapDifficulty() - }; + beatmapInfo = value; + + if (beatmapInfo?.BaseDifficulty != null) + Difficulty = beatmapInfo.BaseDifficulty.Clone(); + } + } + + public Beatmap() + { + beatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = @"Unknown", + Title = @"Unknown", + AuthorString = @"Unknown Creator", + }, + Version = @"Normal", + BaseDifficulty = Difficulty, + }; + } [JsonIgnore] public BeatmapMetadata Metadata => BeatmapInfo?.Metadata ?? BeatmapInfo?.BeatmapSet?.Metadata; diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index f3434c515396..627e54c803df 100755 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -40,7 +40,13 @@ protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset) public IBeatmap Convert(CancellationToken cancellationToken = default) { // We always operate on a clone of the original beatmap, to not modify it game-wide - return ConvertBeatmap(Beatmap.Clone(), cancellationToken); + var original = Beatmap.Clone(); + + // Shallow clone isn't enough to ensure we don't mutate beatmap info unexpectedly. + // Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`. + original.BeatmapInfo = original.BeatmapInfo.Clone(); + + return ConvertBeatmap(original, cancellationToken); } /// diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 2bb0787b4c2c..dfd21469fa1f 100755 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Database; namespace osu.Game.Beatmaps @@ -43,31 +44,31 @@ public float ApproachRate /// public BeatmapDifficulty Clone() { - var diff = new BeatmapDifficulty(); + var diff = (BeatmapDifficulty)Activator.CreateInstance(GetType()); CopyTo(diff); return diff; } - public void CopyFrom(IBeatmapDifficultyInfo difficulty) + public virtual void CopyFrom(IBeatmapDifficultyInfo other) { - ApproachRate = difficulty.ApproachRate; - DrainRate = difficulty.DrainRate; - CircleSize = difficulty.CircleSize; - OverallDifficulty = difficulty.OverallDifficulty; + ApproachRate = other.ApproachRate; + DrainRate = other.DrainRate; + CircleSize = other.CircleSize; + OverallDifficulty = other.OverallDifficulty; - SliderMultiplier = difficulty.SliderMultiplier; - SliderTickRate = difficulty.SliderTickRate; + SliderMultiplier = other.SliderMultiplier; + SliderTickRate = other.SliderTickRate; } - public void CopyTo(BeatmapDifficulty difficulty) + public virtual void CopyTo(BeatmapDifficulty other) { - difficulty.ApproachRate = ApproachRate; - difficulty.DrainRate = DrainRate; - difficulty.CircleSize = CircleSize; - difficulty.OverallDifficulty = OverallDifficulty; + other.ApproachRate = ApproachRate; + other.DrainRate = DrainRate; + other.CircleSize = CircleSize; + other.OverallDifficulty = OverallDifficulty; - difficulty.SliderMultiplier = SliderMultiplier; - difficulty.SliderTickRate = SliderTickRate; + other.SliderMultiplier = SliderMultiplier; + other.SliderTickRate = SliderTickRate; } } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c0bdccdcabdc..99f45b5da465 100755 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -54,7 +54,7 @@ public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, R } } - protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host) + protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider api, GameHost host) { return new BeatmapModelDownloader(modelManager, api, host); } @@ -176,11 +176,6 @@ public Action PostNotification } } - /// - /// Fired when the user requests to view the resulting import. - /// - public Action>> PresentImport { set => beatmapModelManager.PostImport = value; } - /// /// Delete a beatmap difficulty. /// @@ -338,5 +333,14 @@ public void Dispose() } #endregion + + #region Implementation of IPostImports + + public Action>> PostImport + { + set => beatmapModelManager.PostImport = value; + } + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapModelDownloader.cs b/osu.Game/Beatmaps/BeatmapModelDownloader.cs index 92149e8b36b5..f4cec3bfe930 100644 --- a/osu.Game/Beatmaps/BeatmapModelDownloader.cs +++ b/osu.Game/Beatmaps/BeatmapModelDownloader.cs @@ -13,7 +13,7 @@ public class BeatmapModelDownloader : ModelDownloader protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize, bool useSayobot, bool noVideo) => new DownloadBeatmapSetRequest(set, useSayobot, noVideo, minimiseDownloadSize); - public BeatmapModelDownloader(BeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null) + public BeatmapModelDownloader(IBeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null) : base(beatmapModelManager, api, host) { } diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 787559899ada..76019a15aed2 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -192,6 +192,13 @@ public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin { var setInfo = beatmapInfo.BeatmapSet; + // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`. + // This should hopefully be temporary, assuming said clone is eventually removed. + beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty); + + // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding. + beatmapContent.BeatmapInfo = beatmapInfo; + using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) @@ -202,6 +209,7 @@ public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin using (ContextFactory.GetForWrite()) { beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == beatmapInfo.ID); + var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; // grab the original file (or create a new one if not found). diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index efd80a9ca9ec..ab59e9e45fc4 100755 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -15,11 +15,9 @@ public abstract class ControlPoint : IComparable, IDeepCloneable [JsonIgnore] - public double Time => controlPointGroup?.Time ?? 0; + public double Time { get; set; } - private ControlPointGroup controlPointGroup; - - public void AttachGroup(ControlPointGroup pointGroup) => controlPointGroup = pointGroup; + public void AttachGroup(ControlPointGroup pointGroup) => Time = pointGroup.Time; public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time); @@ -46,6 +44,7 @@ public ControlPoint DeepClone() public virtual void CopyFrom(ControlPoint other) { + Time = other.Time; } } } \ No newline at end of file diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 3ff40fe194c3..9d738ecbfb2c 100755 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -33,14 +33,6 @@ public class ControlPointInfo : IDeepCloneable private readonly SortedList timingPoints = new SortedList(Comparer.Default); - /// - /// All difficulty points. - /// - [JsonProperty] - public IReadOnlyList DifficultyPoints => difficultyPoints; - - private readonly SortedList difficultyPoints = new SortedList(Comparer.Default); - /// /// All effect points. /// @@ -55,14 +47,6 @@ public class ControlPointInfo : IDeepCloneable [JsonIgnore] public IEnumerable AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray(); - /// - /// Finds the difficulty control point that is active at . - /// - /// The time to find the difficulty control point at. - /// The difficulty control point. - [NotNull] - public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT); - /// /// Finds the effect control point that is active at . /// @@ -100,7 +84,6 @@ public virtual void Clear() { groups.Clear(); timingPoints.Clear(); - difficultyPoints.Clear(); effectPoints.Clear(); } @@ -277,10 +260,6 @@ protected virtual bool CheckAlreadyExisting(double time, ControlPoint newPoint) case EffectControlPoint _: existing = EffectPointAt(time); break; - - case DifficultyControlPoint _: - existing = DifficultyPointAt(time); - break; } return newPoint?.IsRedundant(existing) == true; @@ -298,9 +277,8 @@ protected virtual void GroupItemAdded(ControlPoint controlPoint) effectPoints.Add(typed); break; - case DifficultyControlPoint typed: - difficultyPoints.Add(typed); - break; + default: + throw new ArgumentException($"A control point of unexpected type {controlPoint.GetType()} was added to this {nameof(ControlPointInfo)}"); } } @@ -315,10 +293,6 @@ protected virtual void GroupItemRemoved(ControlPoint controlPoint) case EffectControlPoint typed: effectPoints.Remove(typed); break; - - case DifficultyControlPoint typed: - difficultyPoints.Remove(typed); - break; } } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 8a6cfaf688a7..bf7ed8e6f58e 100755 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -7,17 +7,20 @@ namespace osu.Game.Beatmaps.ControlPoints { + /// + /// Note that going forward, this control point type should always be assigned directly to HitObjects. + /// public class DifficultyControlPoint : ControlPoint { public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint { - SpeedMultiplierBindable = { Disabled = true }, + SliderVelocityBindable = { Disabled = true }, }; /// - /// The speed multiplier at this control point. + /// The slider velocity at this control point. /// - public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1) + public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1) { Precision = 0.01, Default = 1, @@ -28,21 +31,21 @@ public class DifficultyControlPoint : ControlPoint public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1; /// - /// The speed multiplier at this control point. + /// The slider velocity at this control point. /// - public double SpeedMultiplier + public double SliderVelocity { - get => SpeedMultiplierBindable.Value; - set => SpeedMultiplierBindable.Value = value; + get => SliderVelocityBindable.Value; + set => SliderVelocityBindable.Value = value; } public override bool IsRedundant(ControlPoint existing) => existing is DifficultyControlPoint existingDifficulty - && SpeedMultiplier == existingDifficulty.SpeedMultiplier; + && SliderVelocity == existingDifficulty.SliderVelocity; public override void CopyFrom(ControlPoint other) { - SpeedMultiplier = ((DifficultyControlPoint)other).SpeedMultiplier; + SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity; base.CopyFrom(other); } diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 79bc88e77374..7f550a52fca2 100755 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -12,7 +12,8 @@ public class EffectControlPoint : ControlPoint public static readonly EffectControlPoint DEFAULT = new EffectControlPoint { KiaiModeBindable = { Disabled = true }, - OmitFirstBarLineBindable = { Disabled = true } + OmitFirstBarLineBindable = { Disabled = true }, + ScrollSpeedBindable = { Disabled = true } }; /// @@ -20,6 +21,26 @@ public class EffectControlPoint : ControlPoint /// public readonly BindableBool OmitFirstBarLineBindable = new BindableBool(); + /// + /// The relative scroll speed at this control point. + /// + public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1) + { + Precision = 0.01, + Default = 1, + MinValue = 0.01, + MaxValue = 10 + }; + + /// + /// The relative scroll speed. + /// + public double ScrollSpeed + { + get => ScrollSpeedBindable.Value; + set => ScrollSpeedBindable.Value = value; + } + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple; /// @@ -49,12 +70,14 @@ public override bool IsRedundant(ControlPoint existing) => !OmitFirstBarLine && existing is EffectControlPoint existingEffect && KiaiMode == existingEffect.KiaiMode - && OmitFirstBarLine == existingEffect.OmitFirstBarLine; + && OmitFirstBarLine == existingEffect.OmitFirstBarLine + && ScrollSpeed == existingEffect.ScrollSpeed; public override void CopyFrom(ControlPoint other) { KiaiMode = ((EffectControlPoint)other).KiaiMode; OmitFirstBarLine = ((EffectControlPoint)other).OmitFirstBarLine; + ScrollSpeed = ((EffectControlPoint)other).ScrollSpeed; base.CopyFrom(other); } diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index 4aa6a3d6e994..fb489f73b129 100755 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -8,6 +8,9 @@ namespace osu.Game.Beatmaps.ControlPoints { + /// + /// Note that going forward, this control point type should always be assigned directly to HitObjects. + /// public class SampleControlPoint : ControlPoint { public const string DEFAULT_BANK = "normal"; diff --git a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs index 988968fa42b5..0d5c48f64d4f 100755 --- a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs @@ -18,7 +18,7 @@ protected override void ParseStreamInto(LineBufferedReader stream, Beatmap outpu stream.ReadToEnd().DeserializeInto(output); foreach (var hitObject in output.HitObjects) - hitObject.ApplyDefaults(output.ControlPointInfo, output.BeatmapInfo.BaseDifficulty); + hitObject.ApplyDefaults(output.ControlPointInfo, output.Difficulty); } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 0c8167cc50aa..c2d6e9fe84f9 100755 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -67,7 +67,7 @@ protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatm this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList(); foreach (var hitObject in this.beatmap.HitObjects) - hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.BeatmapInfo.BaseDifficulty); + hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.Difficulty); } protected override bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(' ') || line.StartsWith('_'); @@ -276,7 +276,7 @@ private void handleDifficulty(string line) { var pair = SplitKeyVal(line); - var difficulty = beatmap.BeatmapInfo.BaseDifficulty; + var difficulty = beatmap.Difficulty; switch (pair.Key) { @@ -384,14 +384,21 @@ private void handleTimingPoint(string line) addControlPoint(time, new LegacyDifficultyControlPoint(beatLength) #pragma warning restore 618 { - SpeedMultiplier = speedMultiplier, + SliderVelocity = speedMultiplier, }, timingChange); - addControlPoint(time, new EffectControlPoint + var effectPoint = new EffectControlPoint { KiaiMode = kiaiMode, OmitFirstBarLine = omitFirstBarSignature, - }, timingChange); + }; + + bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0; + // scrolling rulesets use effect points rather than difficulty points for scroll speed adjustments. + if (!isOsuRuleset) + effectPoint.ScrollSpeed = speedMultiplier; + + addControlPoint(time, effectPoint, timingChange); addControlPoint(time, new LegacySampleControlPoint { @@ -444,7 +451,7 @@ private void handleHitObject(string line) if (obj != null) { - obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty); + obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); beatmap.HitObjects.Add(obj); } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index aef13b88722a..1dc270ee63ba 100755 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -141,17 +141,17 @@ private void handleDifficulty(TextWriter writer) { writer.WriteLine("[Difficulty]"); - writer.WriteLine(FormattableString.Invariant($"HPDrainRate: {beatmap.BeatmapInfo.BaseDifficulty.DrainRate}")); - writer.WriteLine(FormattableString.Invariant($"CircleSize: {beatmap.BeatmapInfo.BaseDifficulty.CircleSize}")); - writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty}")); - writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.BeatmapInfo.BaseDifficulty.ApproachRate}")); + writer.WriteLine(FormattableString.Invariant($"HPDrainRate: {beatmap.Difficulty.DrainRate}")); + writer.WriteLine(FormattableString.Invariant($"CircleSize: {beatmap.Difficulty.CircleSize}")); + writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.Difficulty.OverallDifficulty}")); + writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.Difficulty.ApproachRate}")); // Taiko adjusts the slider multiplier (see: LEGACY_TAIKO_VELOCITY_MULTIPLIER) writer.WriteLine(beatmap.BeatmapInfo.RulesetID == 1 - ? FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}") - : FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}")); + ? FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}") + : FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier}")); - writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}")); + writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.Difficulty.SliderTickRate}")); } private void handleEvents(TextWriter writer) @@ -170,33 +170,30 @@ private void handleControlPoints(TextWriter writer) if (beatmap.ControlPointInfo.Groups.Count == 0) return; - writer.WriteLine("[TimingPoints]"); - - if (!(beatmap.ControlPointInfo is LegacyControlPointInfo)) - { - var legacyControlPoints = new LegacyControlPointInfo(); + var legacyControlPoints = new LegacyControlPointInfo(); + foreach (var point in beatmap.ControlPointInfo.AllControlPoints) + legacyControlPoints.Add(point.Time, point.DeepClone()); - foreach (var point in beatmap.ControlPointInfo.AllControlPoints) - legacyControlPoints.Add(point.Time, point.DeepClone()); + writer.WriteLine("[TimingPoints]"); - beatmap.ControlPointInfo = legacyControlPoints; + SampleControlPoint lastRelevantSamplePoint = null; + DifficultyControlPoint lastRelevantDifficultyPoint = null; - SampleControlPoint lastRelevantSamplePoint = null; + bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0; - // iterate over hitobjects and pull out all required sample changes - foreach (var h in beatmap.HitObjects) - { - var hSamplePoint = h.SampleControlPoint; + // iterate over hitobjects and pull out all required sample and difficulty changes + extractDifficultyControlPoints(beatmap.HitObjects); + extractSampleControlPoints(beatmap.HitObjects); - if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint)) - { - legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint); - lastRelevantSamplePoint = hSamplePoint; - } - } + // handle scroll speed, which is stored as "slider velocity" in legacy formats. + // this is relevant for scrolling ruleset beatmaps. + if (!isOsuRuleset) + { + foreach (var point in legacyControlPoints.EffectPoints) + legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed }); } - foreach (var group in beatmap.ControlPointInfo.Groups) + foreach (var group in legacyControlPoints.Groups) { var groupTimingPoint = group.ControlPoints.OfType().FirstOrDefault(); @@ -209,16 +206,16 @@ private void handleControlPoints(TextWriter writer) } // Output any remaining effects as secondary non-timing control point. - var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time); + var difficultyPoint = legacyControlPoints.DifficultyPointAt(group.Time); writer.Write(FormattableString.Invariant($"{group.Time},")); - writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SpeedMultiplier},")); + writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SliderVelocity},")); outputControlPointAt(group.Time, false); } void outputControlPointAt(double time, bool isTimingPoint) { - var samplePoint = ((LegacyControlPointInfo)beatmap.ControlPointInfo).SamplePointAt(time); - var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time); + var samplePoint = legacyControlPoints.SamplePointAt(time); + var effectPoint = legacyControlPoints.EffectPointAt(time); // Apply the control point to a hit sample to uncover legacy properties (e.g. suffix) HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty)); @@ -230,7 +227,7 @@ void outputControlPointAt(double time, bool isTimingPoint) if (effectPoint.OmitFirstBarLine) effectFlags |= LegacyEffectFlags.OmitFirstBarLine; - writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(time).TimeSignature},")); + writer.Write(FormattableString.Invariant($"{(int)legacyControlPoints.TimingPointAt(time).TimeSignature},")); writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},")); writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); @@ -238,6 +235,55 @@ void outputControlPointAt(double time, bool isTimingPoint) writer.Write(FormattableString.Invariant($"{(int)effectFlags}")); writer.WriteLine(); } + + IEnumerable collectDifficultyControlPoints(IEnumerable hitObjects) + { + if (!isOsuRuleset) + yield break; + + foreach (var hitObject in hitObjects) + { + yield return hitObject.DifficultyControlPoint; + + foreach (var nested in collectDifficultyControlPoints(hitObject.NestedHitObjects)) + yield return nested; + } + } + + void extractDifficultyControlPoints(IEnumerable hitObjects) + { + foreach (var hDifficultyPoint in collectDifficultyControlPoints(hitObjects).OrderBy(dp => dp.Time)) + { + if (!hDifficultyPoint.IsRedundant(lastRelevantDifficultyPoint)) + { + legacyControlPoints.Add(hDifficultyPoint.Time, hDifficultyPoint); + lastRelevantDifficultyPoint = hDifficultyPoint; + } + } + } + + IEnumerable collectSampleControlPoints(IEnumerable hitObjects) + { + foreach (var hitObject in hitObjects) + { + yield return hitObject.SampleControlPoint; + + foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects)) + yield return nested; + } + } + + void extractSampleControlPoints(IEnumerable hitObject) + { + foreach (var hSamplePoint in collectSampleControlPoints(hitObject).OrderBy(sp => sp.Time)) + { + if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint)) + { + legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint); + lastRelevantSamplePoint = hSamplePoint; + } + } + } } private void handleColours(TextWriter writer) @@ -285,7 +331,7 @@ private void handleHitObject(TextWriter writer, HitObject hitObject) break; case 3: - int totalColumns = (int)Math.Max(1, beatmap.BeatmapInfo.BaseDifficulty.CircleSize); + int totalColumns = (int)Math.Max(1, beatmap.Difficulty.CircleSize); position.X = (int)Math.Ceiling(((IHasXPosition)hitObject).X * (512f / totalColumns)); break; } diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index ea131150e5d6..0e1c23eeeffb 100755 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -181,7 +181,7 @@ public LegacyDifficultyControlPoint(double beatLength) public LegacyDifficultyControlPoint() { - SpeedMultiplierBindable.Precision = double.Epsilon; + SliderVelocityBindable.Precision = double.Epsilon; } public override void CopyFrom(ControlPoint other) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index f61dd269e1d9..3f598cd1e522 100755 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -20,6 +20,11 @@ public interface IBeatmap /// BeatmapMetadata Metadata { get; } + /// + /// This beatmap's difficulty settings. + /// + public BeatmapDifficulty Difficulty { get; set; } + /// /// The control points in this beatmap. /// diff --git a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs index ff0ca5ebe1a7..2b0a2e7a4d4a 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs @@ -1,9 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using JetBrains.Annotations; using Newtonsoft.Json; -using osu.Framework.Bindables; +using osu.Framework.Lists; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Beatmaps.Legacy @@ -14,9 +15,9 @@ public class LegacyControlPointInfo : ControlPointInfo /// All sound points. /// [JsonProperty] - public IBindableList SamplePoints => samplePoints; + public IReadOnlyList SamplePoints => samplePoints; - private readonly BindableList samplePoints = new BindableList(); + private readonly SortedList samplePoints = new SortedList(Comparer.Default); /// /// Finds the sound control point that is active at . @@ -26,35 +27,76 @@ public class LegacyControlPointInfo : ControlPointInfo [NotNull] public SampleControlPoint SamplePointAt(double time) => BinarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT); + /// + /// All difficulty points. + /// + [JsonProperty] + public IReadOnlyList DifficultyPoints => difficultyPoints; + + private readonly SortedList difficultyPoints = new SortedList(Comparer.Default); + + /// + /// Finds the difficulty control point that is active at . + /// + /// The time to find the difficulty control point at. + /// The difficulty control point. + [NotNull] + public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT); + public override void Clear() { base.Clear(); samplePoints.Clear(); + difficultyPoints.Clear(); } protected override bool CheckAlreadyExisting(double time, ControlPoint newPoint) { - if (newPoint is SampleControlPoint) + switch (newPoint) { - var existing = BinarySearch(SamplePoints, time); - return newPoint.IsRedundant(existing); - } + case SampleControlPoint _: + // intentionally don't use SamplePointAt (we always need to consider the first sample point). + var existing = BinarySearch(SamplePoints, time); + return newPoint.IsRedundant(existing); + + case DifficultyControlPoint _: + return newPoint.IsRedundant(DifficultyPointAt(time)); - return base.CheckAlreadyExisting(time, newPoint); + default: + return base.CheckAlreadyExisting(time, newPoint); + } } protected override void GroupItemAdded(ControlPoint controlPoint) { - if (controlPoint is SampleControlPoint typed) - samplePoints.Add(typed); + switch (controlPoint) + { + case SampleControlPoint typed: + samplePoints.Add(typed); + return; + + case DifficultyControlPoint typed: + difficultyPoints.Add(typed); + return; - base.GroupItemAdded(controlPoint); + default: + base.GroupItemAdded(controlPoint); + break; + } } protected override void GroupItemRemoved(ControlPoint controlPoint) { - if (controlPoint is SampleControlPoint typed) - samplePoints.Remove(typed); + switch (controlPoint) + { + case SampleControlPoint typed: + samplePoints.Remove(typed); + break; + + case DifficultyControlPoint typed: + difficultyPoints.Remove(typed); + break; + } base.GroupItemRemoved(controlPoint); } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 3317acec00a4..2fd8ba0d3d25 100755 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -119,15 +119,12 @@ public virtual IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList m is IApplicableToDifficulty)) { - converted.BeatmapInfo = converted.BeatmapInfo.Clone(); - converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone(); - foreach (var mod in mods.OfType()) { if (cancellationSource.IsCancellationRequested) throw new BeatmapLoadTimeoutException(BeatmapInfo); - mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); + mod.ApplyToDifficulty(converted.Difficulty); } } @@ -146,7 +143,7 @@ public virtual IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList public void CancelAsyncLoad() { - loadCancellation?.Cancel(); - loadCancellation = new CancellationTokenSource(); + lock (beatmapFetchLock) + { + loadCancellation?.Cancel(); + loadCancellation = new CancellationTokenSource(); - if (beatmapLoadTask?.IsCompleted != true) - beatmapLoadTask = null; + if (beatmapLoadTask?.IsCompleted != true) + beatmapLoadTask = null; + } } private CancellationTokenSource createCancellationTokenSource(TimeSpan? timeout) @@ -208,19 +208,27 @@ private CancellationTokenSource createCancellationTokenSource(TimeSpan? timeout) return new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10)); } - private Task loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() => + private readonly object beatmapFetchLock = new object(); + + private Task loadBeatmapAsync() { - // Todo: Handle cancellation during beatmap parsing - var b = GetBeatmap() ?? new Beatmap(); + lock (beatmapFetchLock) + { + return beatmapLoadTask ??= Task.Factory.StartNew(() => + { + // Todo: Handle cancellation during beatmap parsing + var b = GetBeatmap() ?? new Beatmap(); - // The original beatmap version needs to be preserved as the database doesn't contain it - BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion; + // The original beatmap version needs to be preserved as the database doesn't contain it + BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion; - // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc) - b.BeatmapInfo = BeatmapInfo; + // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc) + b.BeatmapInfo = BeatmapInfo; - return b; - }, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + return b; + }, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + } public override string ToString() => BeatmapInfo.ToString(); diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index ad3e890b3aa1..cf83345e2aa1 100755 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -66,8 +66,12 @@ public void Invalidate(BeatmapInfo info) lock (workingCache) { var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID); + if (working != null) + { + Logger.Log($"Invalidating working beatmap cache for {info}"); workingCache.Remove(working); + } } } @@ -86,6 +90,7 @@ public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) lock (workingCache) { var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); + if (working != null) return working; diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 20dc1dd61ff3..9372fea42d40 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Game.Audio.Effects; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -20,6 +22,8 @@ public class ManageCollectionsDialog : OsuFocusedOverlayContainer private const double enter_duration = 500; private const double exit_duration = 200; + private AudioFilter lowPassFilter; + [Resolved(CanBeNull = true)] private CollectionManager collectionManager { get; set; } @@ -36,7 +40,7 @@ public ManageCollectionsDialog() } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { Children = new Drawable[] { @@ -108,7 +112,8 @@ private void load(OsuColour colours) }, } } - } + }, + lowPassFilter = new AudioFilter(audio.TrackMixer) }; } @@ -116,6 +121,7 @@ protected override void PopIn() { base.PopIn(); + lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); this.FadeIn(enter_duration, Easing.OutQuint); this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint); } @@ -124,6 +130,8 @@ protected override void PopOut() { base.PopOut(); + lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); + this.FadeOut(exit_duration, Easing.OutQuint); this.ScaleTo(0.9f, exit_duration); diff --git a/osu.Game/Configuration/MConfigManager.cs b/osu.Game/Configuration/MConfigManager.cs index 1753f532971a..ac3e92abc718 100644 --- a/osu.Game/Configuration/MConfigManager.cs +++ b/osu.Game/Configuration/MConfigManager.cs @@ -34,7 +34,6 @@ protected override void InitialiseDefaults() SetDefault(MSetting.OptUI, true); SetDefault(MSetting.TrianglesEnabled, true); SetDefault(MSetting.SongSelectBgBlur, 0.2f, 0f, 1f); - SetDefault(MSetting.AlwaysHideTextIndicator, false); //Intro Settings SetDefault(MSetting.IntroLoadDirectToSongSelect, false); @@ -128,7 +127,6 @@ public enum MSetting FadeInWindowWhenEntering, UseSystemCursor, PreferredFont, - AlwaysHideTextIndicator, MvisCurrentAudioProvider, Gamemode, DoNotShowDisclaimer, diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 0fdc2dc22eca..f9e8a535551f 100755 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager, IPostImports + public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : class, INamedFileInfo, new() { @@ -116,7 +116,7 @@ protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFa /// One or more archive locations on disk. public Task Import(params string[] paths) { - var notification = new ProgressNotification { State = ProgressNotificationState.Active }; + var notification = new ImportProgressNotification(); PostNotification?.Invoke(notification); @@ -125,7 +125,7 @@ public Task Import(params string[] paths) public Task Import(params ImportTask[] tasks) { - var notification = new ProgressNotification { State = ProgressNotificationState.Active }; + var notification = new ImportProgressNotification(); PostNotification?.Invoke(notification); diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs new file mode 100644 index 000000000000..024d9f2a895e --- /dev/null +++ b/osu.Game/Database/IHasRealmFiles.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Models; + +#nullable enable + +namespace osu.Game.Database +{ + /// + /// A model that contains a list of files it is responsible for. + /// + public interface IHasRealmFiles + { + IList Files { get; } + + string Hash { get; set; } + } +} diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index e94af01772ae..479f33c3b460 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -10,10 +10,10 @@ namespace osu.Game.Database { /// - /// A class which handles importing of asociated models to the game store. + /// A class which handles importing of associated models to the game store. /// /// The model type. - public interface IModelImporter : IPostNotifications + public interface IModelImporter : IPostNotifications, IPostImports where TModel : class { /// diff --git a/osu.Game/Database/INamedFile.cs b/osu.Game/Database/INamedFile.cs new file mode 100644 index 000000000000..2bd45d4e4243 --- /dev/null +++ b/osu.Game/Database/INamedFile.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Models; + +#nullable enable + +namespace osu.Game.Database +{ + /// + /// Represents a join model which gives a filename and scope to a . + /// + public interface INamedFile + { + string Filename { get; set; } + + RealmFile File { get; set; } + } +} diff --git a/osu.Game/Database/ImportProgressNotification.cs b/osu.Game/Database/ImportProgressNotification.cs new file mode 100644 index 000000000000..aaee3e117f0d --- /dev/null +++ b/osu.Game/Database/ImportProgressNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Database +{ + public class ImportProgressNotification : ProgressNotification + { + public ImportProgressNotification() + { + State = ProgressNotificationState.Active; + } + } +} diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0ff902a8bcdf..82d51e365e2c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -5,7 +5,6 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Development; -using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; @@ -18,7 +17,7 @@ namespace osu.Game.Database /// /// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage. /// - public class RealmContextFactory : Component, IRealmFactory + public class RealmContextFactory : IDisposable, IRealmFactory { private readonly Storage storage; @@ -79,10 +78,11 @@ public RealmContextFactory(Storage storage, string filename) /// public bool Compact() => Realm.Compact(getConfiguration()); - protected override void Update() + /// + /// Perform a blocking refresh on the main realm context. + /// + public void Refresh() { - base.Update(); - lock (contextLock) { if (context?.Refresh() == true) @@ -92,7 +92,7 @@ protected override void Update() public Realm CreateContext() { - if (IsDisposed) + if (isDisposed) throw new ObjectDisposedException(nameof(RealmContextFactory)); try @@ -132,12 +132,11 @@ private void onMigration(Migration migration, ulong lastSchemaVersion) /// An which should be disposed to end the blocking section. public IDisposable BlockAllOperations() { - if (IsDisposed) + if (isDisposed) throw new ObjectDisposedException(nameof(RealmContextFactory)); - // TODO: this can be added for safety once we figure how to bypass in test - // if (!ThreadSafety.IsUpdateThread) - // throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread."); + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread."); Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); @@ -177,21 +176,23 @@ public IDisposable BlockAllOperations() }); } - protected override void Dispose(bool isDisposing) + private bool isDisposed; + + public void Dispose() { lock (contextLock) { context?.Dispose(); } - if (!IsDisposed) + if (!isDisposed) { // intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal. contextCreationLock.Wait(); contextCreationLock.Dispose(); - } - base.Dispose(isDisposing); + isDisposed = true; + } } } } diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs new file mode 100644 index 000000000000..abb69644d655 --- /dev/null +++ b/osu.Game/Database/RealmLive.cs @@ -0,0 +1,111 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Threading; +using Realms; + +#nullable enable + +namespace osu.Game.Database +{ + /// + /// Provides a method of working with realm objects over longer application lifetimes. + /// + /// The underlying object type. + public class RealmLive : ILive where T : RealmObject, IHasGuidPrimaryKey + { + public Guid ID { get; } + + private readonly SynchronizationContext? fetchedContext; + private readonly int fetchedThreadId; + + /// + /// The original live data used to create this instance. + /// + private readonly T data; + + /// + /// Construct a new instance of live realm data. + /// + /// The realm data. + public RealmLive(T data) + { + this.data = data; + + fetchedContext = SynchronizationContext.Current; + fetchedThreadId = Thread.CurrentThread.ManagedThreadId; + + ID = data.ID; + } + + /// + /// Perform a read operation on this live object. + /// + /// The action to perform. + public void PerformRead(Action perform) + { + if (originalDataValid) + { + perform(data); + return; + } + + using (var realm = Realm.GetInstance(data.Realm.Config)) + perform(realm.Find(ID)); + } + + /// + /// Perform a read operation on this live object. + /// + /// The action to perform. + public TReturn PerformRead(Func perform) + { + if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn))) + throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}."); + + if (originalDataValid) + return perform(data); + + using (var realm = Realm.GetInstance(data.Realm.Config)) + return perform(realm.Find(ID)); + } + + /// + /// Perform a write operation on this live object. + /// + /// The action to perform. + public void PerformWrite(Action perform) => + PerformRead(t => + { + var transaction = t.Realm.BeginWrite(); + perform(t); + transaction.Commit(); + }); + + public T Value + { + get + { + if (originalDataValid) + return data; + + T retrieved; + + using (var realm = Realm.GetInstance(data.Realm.Config)) + retrieved = realm.Find(ID); + + if (!retrieved.IsValid) + throw new InvalidOperationException("Attempted to access value without an open context"); + + return retrieved; + } + } + + private bool originalDataValid => isCorrectThread && data.IsValid; + + // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72) + private bool isCorrectThread + => (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId; + } +} diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index c5aa1399a3a0..18a926fa8c46 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using AutoMapper; using osu.Game.Input.Bindings; using Realms; @@ -47,5 +48,17 @@ public static T Detach(this T item) where T : RealmObject return mapper.Map(item); } + + public static List> ToLive(this IEnumerable realmList) + where T : RealmObject, IHasGuidPrimaryKey + { + return realmList.Select(l => new RealmLive(l)).ToList(); + } + + public static RealmLive ToLive(this T realmObject) + where T : RealmObject, IHasGuidPrimaryKey + { + return new RealmLive(realmObject); + } } } diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index aaad72f65ccf..017ea6ec32ff 100755 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -1,11 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -141,12 +144,12 @@ public OsuScrollbar(Direction scrollDir) Child = box = new Box { RelativeSizeAxes = Axes.Both }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, OsuColour colours) { Colour = defaultColour = colours.Gray8; hoverColour = colours.GrayF; - highlightColour = colours.Green; + highlightColour = colourProvider?.Highlight1 ?? colours.Green; } public override void ResizeTo(float val, int duration = 0, Easing easing = Easing.None) diff --git a/osu.Game/Graphics/Mf/FontInfoLabel.cs b/osu.Game/Graphics/Mf/FontInfoLabel.cs index bbb2352d9a71..02e92f6f46ec 100644 --- a/osu.Game/Graphics/Mf/FontInfoLabel.cs +++ b/osu.Game/Graphics/Mf/FontInfoLabel.cs @@ -25,7 +25,7 @@ public FontInfoLabel(Font font) { bg = new Box { - Colour = Color4Extensions.FromHex("#111"), + Colour = Color4Extensions.FromHex("#18171c"), RelativeSizeAxes = Axes.Both }, new FillFlowContainer @@ -71,7 +71,7 @@ protected override bool OnHover(HoverEvent e) protected override void OnHoverLost(HoverLostEvent e) { - bg.FadeColour(Color4Extensions.FromHex("#111"), 300); + bg.FadeColour(Color4Extensions.FromHex("#18171c"), 300); base.OnHoverLost(e); } diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 5648ef64ab95..9313d2c82727 100755 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -225,6 +225,16 @@ public static Color4 ForegroundTextColourFor(Color4 backgroundColour) public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee"); public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff"); + /// + /// Equivalent to 's . + /// + public readonly Color4 Pink3 = Color4Extensions.FromHex(@"cc3378"); + + /// + /// Equivalent to 's . + /// + public readonly Color4 Blue3 = Color4Extensions.FromHex(@"3399cc"); + /// /// Equivalent to 's . /// diff --git a/osu.Game/Graphics/TextEditIndicator.cs b/osu.Game/Graphics/TextEditIndicator.cs deleted file mode 100644 index 96f67bfd7f89..000000000000 --- a/osu.Game/Graphics/TextEditIndicator.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System.Text; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Configuration; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Mvis.SideBar.Settings.Items; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Graphics -{ - public class TextEditIndicator : VisibilityContainer - { - private readonly OsuSpriteText spriteText; - private readonly ProgressBar bg; - private readonly Box flashBox; - private readonly Bindable optUI = new Bindable(); - private readonly Bindable alwaysHide = new Bindable(); - private readonly FillFlowContainer placeHolderContainer; - private readonly Circle bar; - - [Resolved(canBeNull: true)] - private OsuGame game { get; set; } - - private string text; - - public string Text - { - get => text; - set - { - if (value == text) - return; - - text = value; - - //猜测: 因为文本输入在input上,因此不使用Schedule()会导致osu.Framework.Graphics.Drawable+InvalidThreadForMutationException - - if (string.IsNullOrEmpty(value)) - { - if (State.Value == Visibility.Visible) - Schedule(executeTimeoutHide); - - Schedule(() => - { - placeHolderContainer.FadeIn(150); - spriteText.FadeOut(150); - }); - } - else - { - Schedule(() => - { - abortTimeoutHide(true); - - placeHolderContainer.FadeOut(150); - spriteText.FadeIn(150); - }); - } - - Schedule(() => - { - spriteText.Text = value; - bg.CurrentTime = Encoding.Default.GetBytes(Text).Length; - }); - } - } - - private void executeTimeoutHide() => - bar.ResizeWidthTo(0, 3000).OnComplete(_ => Hide()); - - private void abortTimeoutHide(bool animate) => - bar.ResizeWidthTo(0.8f, animate ? 300 : 0, Easing.OutQuint); - - public TextEditIndicator() - { - AutoSizeAxes = Axes.Both; - Anchor = Anchor.TopCentre; - Origin = Anchor.TopCentre; - Masking = true; - CornerRadius = 5f; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Radius = 7, - Colour = Color4.Black.Opacity(0.1f) - }; - - Children = new Drawable[] - { - bg = new ByteLengthIndicator(false) - { - RelativeSizeAxes = Axes.Both, - BackgroundColour = Color4.Black.Opacity(0.5f), - FillColour = Color4.Aqua.Opacity(0.5f), - EndTime = 31, - CurrentTime = 0 - }, - flashBox = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Gold.Opacity(0f), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Blending = BlendingParameters.Mixture - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - AutoSizeDuration = 300, - AutoSizeEasing = Easing.OutQuint, - Margin = new MarginPadding(10), - Spacing = new Vector2(3), - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Children = new Drawable[] - { - new PlaceHolder - { - Height = 28 - }, - placeHolderContainer = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Spacing = new Vector2(5), - Margin = new MarginPadding { Vertical = 5 }, - Colour = Color4.White.Opacity(0.6f), - Children = new Drawable[] - { - new SpriteIcon - { - Size = new Vector2(14), - Icon = FontAwesome.Solid.Pen, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Vertical = 2 } - }, - new OsuSpriteText - { - Text = "暂无输入", - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - } - } - }, - spriteText = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding { Vertical = 5 }, - }, - } - }, - bar = new Circle - { - Height = 3, - RelativeSizeAxes = Axes.X, - Width = 0.8f, - Colour = Color4.White, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - new OsuSpriteText - { - Text = "若输入法完成编辑后没有出现文字,请尝试多按几次空格", - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - } - } - } - }; - } - - [BackgroundDependencyLoader] - private void load(MConfigManager config) - { - config.BindWith(MSetting.OptUI, optUI); - config.BindWith(MSetting.AlwaysHideTextIndicator, alwaysHide); - - optUI.BindValueChanged(v => - { - if (!v.NewValue) Hide(); - else if (!string.IsNullOrEmpty(Text)) Show(); - }); - - alwaysHide.BindValueChanged(v => - { - if (v.NewValue) Hide(); - }); - } - - protected override void UpdateAfterChildren() - { - Margin = new MarginPadding { Top = (game?.ToolbarOffset ?? 0) + 5 }; - base.UpdateAfterChildren(); - } - - public override void Show() - { - if (!optUI.Value || alwaysHide.Value) return; - - base.Show(); - } - - protected override void PopIn() - { - var emptyText = string.IsNullOrEmpty(Text); - abortTimeoutHide(!emptyText); - - //在某些系统下窗口会莫名进入编辑状态,此时因为没有文本,所以不要显示 - if (emptyText) return; - - this.FadeIn(300, Easing.OutSine) - .MoveToY(0, 300, Easing.OutQuint); - } - - protected override void PopOut() - { - this.FadeOut(300, Easing.OutQuint) - .MoveToY(-23, 300, Easing.OutQuint); - } - - public void Flash() => - flashBox.FlashColour(Color4.Gold, 1000, Easing.OutQuint); - - private class ByteLengthIndicator : ProgressBar - { - protected override void UpdateValue(float value) - { - fill.ResizeWidthTo(value, 300, Easing.OutQuint); - fill.FadeColour(CurrentNumber.Value >= CurrentNumber.MaxValue - ? Color4.Red.Opacity(0.5f) - : Color4.Aqua.Opacity(0.5f), 300); - } - - public ByteLengthIndicator(bool allowSeek) - : base(allowSeek) - { - fill.RelativeSizeAxes = Axes.Both; - } - } - } -} diff --git a/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs new file mode 100644 index 000000000000..4e1c612f090e --- /dev/null +++ b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Graphics.UserInterface +{ + public abstract class CommaSeparatedScoreCounter : RollingCounter + { + protected override double RollingDuration => 1000; + protected override Easing RollingEasing => Easing.Out; + + protected override double GetProportionalDuration(double currentValue, double newValue) => + currentValue > newValue ? currentValue - newValue : newValue - currentValue; + + protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(@"N0"); + + protected override OsuSpriteText CreateSpriteText() + => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); + } +} diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs index 6807d007bb9e..8f0fed580f05 100755 --- a/osu.Game/Graphics/UserInterface/Nub.cs +++ b/osu.Game/Graphics/UserInterface/Nub.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; @@ -12,63 +13,74 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterface { - public class Nub : CircularContainer, IHasCurrentValue, IHasAccentColour + public class Nub : CompositeDrawable, IHasCurrentValue, IHasAccentColour { - public const float COLLAPSED_SIZE = 20; - public const float EXPANDED_SIZE = 40; + public const float HEIGHT = 15; + + public const float EXPANDED_SIZE = 50; private const float border_width = 3; - private const double animate_in_duration = 150; + private const double animate_in_duration = 200; private const double animate_out_duration = 500; + private readonly Box fill; + private readonly Container main; + public Nub() { - Box fill; - - Size = new Vector2(COLLAPSED_SIZE, 12); - - BorderColour = Color4.White; - BorderThickness = border_width; + Size = new Vector2(EXPANDED_SIZE, HEIGHT); - Masking = true; - - Children = new[] + InternalChildren = new[] { - fill = new Box + main = new CircularContainer { + BorderColour = Color4.White, + BorderThickness = border_width, + Masking = true, RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Children = new Drawable[] + { + fill = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + }, + } }, }; - - Current.ValueChanged += filled => - { - fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint); - this.TransformTo(nameof(BorderThickness), filled.NewValue ? 8.5f : border_width, 200, Easing.OutQuint); - }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour colours) { - AccentColour = colours.Pink; - GlowingAccentColour = colours.PinkLighter; - GlowColour = colours.PinkDarker; + AccentColour = colourProvider?.Highlight1 ?? colours.Pink; + GlowingAccentColour = colourProvider?.Highlight1.Lighten(0.2f) ?? colours.PinkLighter; + GlowColour = colourProvider?.Highlight1 ?? colours.PinkLighter; - EdgeEffect = new EdgeEffectParameters + main.EdgeEffect = new EdgeEffectParameters { Colour = GlowColour.Opacity(0), Type = EdgeEffectType.Glow, - Radius = 10, - Roundness = 8, + Radius = 8, + Roundness = 5, }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(onCurrentValueChanged, true); + } + private bool glowing; public bool Glowing @@ -80,28 +92,17 @@ public bool Glowing if (value) { - this.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint); - FadeEdgeEffectTo(1, animate_in_duration, Easing.OutQuint); + main.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint); + main.FadeEdgeEffectTo(0.2f, animate_in_duration, Easing.OutQuint); } else { - FadeEdgeEffectTo(0, animate_out_duration); - this.FadeColour(AccentColour, animate_out_duration); + main.FadeEdgeEffectTo(0, animate_out_duration, Easing.OutQuint); + main.FadeColour(AccentColour, animate_out_duration, Easing.OutQuint); } } } - public bool Expanded - { - set - { - if (value) - this.ResizeTo(new Vector2(EXPANDED_SIZE, 12), animate_in_duration, Easing.OutQuint); - else - this.ResizeTo(new Vector2(COLLAPSED_SIZE, 12), animate_out_duration, Easing.OutQuint); - } - } - private readonly Bindable current = new Bindable(); public Bindable Current @@ -126,7 +127,7 @@ public Color4 AccentColour { accentColour = value; if (!Glowing) - Colour = value; + main.Colour = value; } } @@ -139,7 +140,7 @@ public Color4 GlowingAccentColour { glowingAccentColour = value; if (Glowing) - Colour = value; + main.Colour = value; } } @@ -152,10 +153,22 @@ public Color4 GlowColour { glowColour = value; - var effect = EdgeEffect; + var effect = main.EdgeEffect; effect.Colour = Glowing ? value : value.Opacity(0); - EdgeEffect = effect; + main.EdgeEffect = effect; } } + + private void onCurrentValueChanged(ValueChangedEvent filled) + { + fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint); + + if (filled.NewValue) + main.ResizeWidthTo(1, animate_in_duration, Easing.OutElasticHalf); + else + main.ResizeWidthTo(0.9f, animate_out_duration, Easing.OutElastic); + + main.TransformTo(nameof(BorderThickness), filled.NewValue ? 8.5f : border_width, 200, Easing.OutQuint); + } } } diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 5f2d884cd766..e8f80dec57e5 100755 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -9,16 +9,11 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; -using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { public class OsuCheckbox : Checkbox { - public Color4 CheckedColor { get; set; } = Color4.Cyan; - public Color4 UncheckedColor { get; set; } = Color4.White; - public int FadeDuration { get; set; } - /// /// Whether to play sounds when the state changes as a result of user interaction. /// @@ -104,14 +99,12 @@ private void load(AudioManager audio) protected override bool OnHover(HoverEvent e) { Nub.Glowing = true; - Nub.Expanded = true; return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { Nub.Glowing = false; - Nub.Expanded = false; base.OnHoverLost(e); } diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index fe88e6f78a58..5831d9ab1f44 100755 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -1,8 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System.Linq; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -14,13 +15,15 @@ using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osuTK; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { public class OsuDropdown : Dropdown, IHasAccentColour { - private const float corner_radius = 4; + private const float corner_radius = 5; private Color4 accentColour; @@ -34,11 +37,11 @@ public Color4 AccentColour } } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, OsuColour colours) { if (accentColour == default) - accentColour = colours.PinkDarker; + accentColour = colourProvider?.Light4 ?? colours.PinkDarker; updateAccentColour(); } @@ -59,14 +62,13 @@ protected class OsuDropdownMenu : DropdownMenu, IHasAccentColour { public override bool HandleNonPositionalInput => State == MenuState.Open; - private Sample sampleOpen; - private Sample sampleClose; + private Sample? sampleOpen; + private Sample? sampleClose; // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring public OsuDropdownMenu() { CornerRadius = corner_radius; - BackgroundColour = Color4.Black.Opacity(0.5f); MaskingContainer.CornerRadius = corner_radius; Alpha = 0; @@ -75,9 +77,11 @@ public OsuDropdownMenu() ItemsContainer.Padding = new MarginPadding(5); } - [BackgroundDependencyLoader] - private void load(AudioManager audio) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, AudioManager audio) { + BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); sampleClose = audio.Samples.Get(@"UI/dropdown-close"); } @@ -159,6 +163,8 @@ private void updateColours() { BackgroundColourHover = accentColour ?? nonAccentHoverColour; BackgroundColourSelected = accentColour ?? nonAccentSelectedColour; + BackgroundColour = BackgroundColourHover.Opacity(0); + UpdateBackgroundColour(); UpdateForegroundColour(); } @@ -178,8 +184,6 @@ public DrawableOsuDropdownMenuItem(MenuItem item) [BackgroundDependencyLoader] private void load(OsuColour colours) { - BackgroundColour = Color4.Transparent; - nonAccentHoverColour = colours.PinkDarker; nonAccentSelectedColour = Color4.Black.Opacity(0.5f); updateColours(); @@ -187,16 +191,29 @@ private void load(OsuColour colours) AddInternal(new HoverSounds()); } + protected override void UpdateBackgroundColour() + { + if (!IsPreSelected && !IsSelected) + { + Background.FadeOut(600, Easing.OutQuint); + return; + } + + Background.FadeIn(100, Easing.OutQuint); + Background.FadeColour(IsPreSelected ? BackgroundColourHover : BackgroundColourSelected, 100, Easing.OutQuint); + } + protected override void UpdateForegroundColour() { base.UpdateForegroundColour(); - if (Foreground.Children.FirstOrDefault() is Content content) content.Chevron.Alpha = IsHovered ? 1 : 0; + if (Foreground.Children.FirstOrDefault() is Content content) + content.Hovering = IsHovered; } protected override Drawable CreateContent() => new Content(); - protected new class Content : FillFlowContainer, IHasText + protected new class Content : CompositeDrawable, IHasText { public LocalisableString Text { @@ -207,32 +224,64 @@ public LocalisableString Text public readonly OsuSpriteText Label; public readonly SpriteIcon Chevron; + private const float chevron_offset = -3; + public Content() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Direction = FillDirection.Horizontal; - Children = new Drawable[] + InternalChildren = new Drawable[] { Chevron = new SpriteIcon { - AlwaysPresent = true, Icon = FontAwesome.Solid.ChevronRight, - Colour = Color4.Black, - Alpha = 0.5f, Size = new Vector2(8), + Alpha = 0, + X = chevron_offset, Margin = new MarginPadding { Left = 3, Right = 3 }, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, Label = new OsuSpriteText { + X = 15, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, }; } + + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider) + { + Chevron.Colour = colourProvider?.Background5 ?? Color4.Black; + } + + private bool hovering; + + public bool Hovering + { + get => hovering; + set + { + if (value == hovering) + return; + + hovering = value; + + if (hovering) + { + Chevron.FadeIn(400, Easing.OutQuint); + Chevron.MoveToX(0, 400, Easing.OutQuint); + } + else + { + Chevron.FadeOut(200); + Chevron.MoveToX(chevron_offset, 200, Easing.In); + } + } + } } } @@ -267,7 +316,7 @@ public virtual Color4 AccentColour public OsuDropdownHeader() { - Foreground.Padding = new MarginPadding(4); + Foreground.Padding = new MarginPadding(10); AutoSizeAxes = Axes.None; Margin = new MarginPadding { Bottom = 4 }; @@ -303,8 +352,7 @@ public OsuDropdownHeader() Icon = FontAwesome.Solid.ChevronDown, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Margin = new MarginPadding { Horizontal = 5 }, - Size = new Vector2(12), + Size = new Vector2(16), }, } } @@ -313,11 +361,11 @@ public OsuDropdownHeader() AddInternal(new HoverClickSounds()); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, OsuColour colours) { - BackgroundColour = Color4.Black.Opacity(0.5f); - BackgroundColourHover = colours.PinkDarker; + BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + BackgroundColourHover = colourProvider?.Light4 ?? colours.PinkDarker; } } } diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index f85f9327fa2f..6963f7335e49 100755 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -3,11 +3,13 @@ using System; using System.Globalization; +using JetBrains.Annotations; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -16,6 +18,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterface { @@ -52,34 +55,63 @@ public Color4 AccentColour { accentColour = value; leftBox.Colour = value; + } + } + + private Colour4 backgroundColour; + + public Color4 BackgroundColour + { + get => backgroundColour; + set + { + backgroundColour = value; rightBox.Colour = value; } } public OsuSliderBar() { - Height = 12; - RangePadding = 20; + Height = Nub.HEIGHT; + RangePadding = Nub.EXPANDED_SIZE / 2; Children = new Drawable[] { - leftBox = new Box + new Container { - Height = 2, - EdgeSmoothness = new Vector2(0, 0.5f), - Position = new Vector2(2, 0), - RelativeSizeAxes = Axes.None, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - }, - rightBox = new Box - { - Height = 2, - EdgeSmoothness = new Vector2(0, 0.5f), - Position = new Vector2(-2, 0), - RelativeSizeAxes = Axes.None, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Alpha = 0.5f, + Padding = new MarginPadding { Horizontal = 2 }, + Child = new CircularContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Masking = true, + CornerRadius = 5f, + Children = new Drawable[] + { + leftBox = new Box + { + Height = 5, + EdgeSmoothness = new Vector2(0, 0.5f), + RelativeSizeAxes = Axes.None, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + rightBox = new Box + { + Height = 5, + EdgeSmoothness = new Vector2(0, 0.5f), + RelativeSizeAxes = Axes.None, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Alpha = 0.5f, + }, + }, + }, }, nubContainer = new Container { @@ -88,7 +120,7 @@ public OsuSliderBar() { Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, - Expanded = true, + Current = { Value = true } }, }, new HoverClickSounds() @@ -97,11 +129,12 @@ public OsuSliderBar() Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; }; } - [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(AudioManager audio, [CanBeNull] OverlayColourProvider colourProvider, OsuColour colours) { sample = audio.Samples.Get(@"UI/notch-tick"); - AccentColour = colours.Pink; + AccentColour = colourProvider?.Highlight1 ?? colours.Pink; + BackgroundColour = colourProvider?.Background5 ?? colours.Pink.Opacity(0.5f); } protected override void Update() @@ -119,26 +152,25 @@ protected override void LoadComplete() protected override bool OnHover(HoverEvent e) { - Nub.Glowing = true; + updateGlow(); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - Nub.Glowing = false; + updateGlow(); base.OnHoverLost(e); } - protected override bool OnMouseDown(MouseDownEvent e) + protected override void OnDragEnd(DragEndEvent e) { - Nub.Current.Value = true; - return base.OnMouseDown(e); + updateGlow(); + base.OnDragEnd(e); } - protected override void OnMouseUp(MouseUpEvent e) + private void updateGlow() { - Nub.Current.Value = false; - base.OnMouseUp(e); + Nub.Glowing = IsHovered || IsDragged; } protected override void OnUserChange(T value) diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index 7ebf3819e4c7..25f19aa0a986 100755 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; @@ -13,43 +14,30 @@ public abstract class ScoreCounter : RollingCounter protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; - /// - /// Whether comma separators should be displayed. - /// - public bool UseCommaSeparator { get; } - public Bindable RequiredDisplayDigits { get; } = new Bindable(); + private string formatString; + /// /// Displays score. /// /// How many leading zeroes the counter will have. - /// Whether comma separators should be displayed. - protected ScoreCounter(int leading = 0, bool useCommaSeparator = false) + protected ScoreCounter(int leading = 0) { - UseCommaSeparator = useCommaSeparator; - RequiredDisplayDigits.Value = leading; - RequiredDisplayDigits.BindValueChanged(_ => UpdateDisplay()); + RequiredDisplayDigits.BindValueChanged(displayDigitsChanged, true); } - protected override double GetProportionalDuration(double currentValue, double newValue) + private void displayDigitsChanged(ValueChangedEvent _) { - return currentValue > newValue ? currentValue - newValue : newValue - currentValue; + formatString = new string('0', RequiredDisplayDigits.Value); + UpdateDisplay(); } - protected override LocalisableString FormatCount(double count) - { - string format = new string('0', RequiredDisplayDigits.Value); - - if (UseCommaSeparator) - { - for (int i = format.Length - 3; i > 0; i -= 3) - format = format.Insert(i, @","); - } + protected override double GetProportionalDuration(double currentValue, double newValue) => + currentValue > newValue ? currentValue - newValue : newValue - currentValue; - return ((long)count).ToString(format); - } + protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(formatString); protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); diff --git a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs index 965734792c6a..c01ee1a05980 100644 --- a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs @@ -2,11 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { @@ -15,30 +12,13 @@ public class SlimEnumDropdown : OsuEnumDropdown { protected override DropdownHeader CreateHeader() => new SlimDropdownHeader(); - protected override DropdownMenu CreateMenu() => new SlimMenu(); - private class SlimDropdownHeader : OsuDropdownHeader { public SlimDropdownHeader() { Height = 25; - Icon.Size = new Vector2(16); Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 }; } - - protected override void LoadComplete() - { - base.LoadComplete(); - BackgroundColour = Color4.Black.Opacity(0.25f); - } - } - - private class SlimMenu : OsuDropdownMenu - { - public SlimMenu() - { - BackgroundColour = Color4.Black.Opacity(0.7f); - } } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs index 18869ad06c8a..7dbb5cc45c70 100755 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 @@ -44,6 +47,7 @@ public float? FixedLabelWidth /// protected readonly T Component; + private readonly Box background; private readonly GridContainer grid; private readonly OsuTextFlowContainer labelText; private readonly OsuTextFlowContainer descriptionText; @@ -62,10 +66,9 @@ protected LabelledDrawable(bool padded) InternalChildren = new Drawable[] { - new Box + background = new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("1c2125"), }, new FillFlowContainer { @@ -146,9 +149,10 @@ private void updateLabelWidth() } } - [BackgroundDependencyLoader] - private void load(OsuColour osuColour) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, OsuColour osuColour) { + background.Colour = colourProvider?.Background4 ?? Color4Extensions.FromHex(@"1c2125"); descriptionText.Colour = osuColour.Yellow; } diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs new file mode 100644 index 000000000000..23ebc6e98de4 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class RoundedButton : OsuButton, IFilterable + { + public override float Height + { + get => base.Height; + set + { + base.Height = value; + + if (IsLoaded) + updateCornerRadius(); + } + } + + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours) + { + BackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateCornerRadius(); + } + + private void updateCornerRadius() => Content.CornerRadius = DrawHeight / 2; + + public virtual IEnumerable FilterTerms => new[] { Text.ToString() }; + + public bool MatchingFilter + { + set => this.FadeTo(value ? 1 : 0); + } + + public bool FilteringActive { get; set; } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs index a7fd25b554d3..deb2e6baf66b 100755 --- a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -10,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -66,11 +69,11 @@ public SwitchButton() }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, OsuColour colours) { - enabledColour = colours.BlueDark; - disabledColour = colours.Gray3; + enabledColour = colourProvider?.Highlight1 ?? colours.BlueDark; + disabledColour = colourProvider?.Background3 ?? colours.Gray3; switchContainer.Colour = enabledColour; fill.Colour = disabledColour; diff --git a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs new file mode 100644 index 000000000000..f5709b515869 --- /dev/null +++ b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; + +namespace osu.Game.IO.FileAbstraction +{ + public class StreamFileAbstraction : TagLib.File.IFileAbstraction + { + public StreamFileAbstraction(string filename, Stream fileStream) + { + ReadStream = fileStream; + Name = filename; + } + + public string Name { get; } + + public Stream ReadStream { get; } + public Stream WriteStream => ReadStream; + + public void CloseStream(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + stream.Close(); + } + } +} diff --git a/osu.Game/Localisation/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs index c9c8d9c43f52..f3c577997a7e 100644 --- a/osu.Game/Localisation/AudioSettingsStrings.cs +++ b/osu.Game/Localisation/AudioSettingsStrings.cs @@ -24,6 +24,11 @@ public static class AudioSettingsStrings /// public static LocalisableString VolumeHeader => new TranslatableString(getKey(@"volume_header"), @"音量"); + /// + /// "Output device" + /// + public static LocalisableString OutputDevice => new TranslatableString(getKey(@"output_device"), @"Output device"); + /// /// "Master" /// diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 6efb7884bc17..875f99af7331 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -14,11 +14,36 @@ public static class GameplaySettingsStrings /// public static LocalisableString GameplaySectionHeader => new TranslatableString(getKey(@"gameplay_section_header"), @"Gameplay"); + /// + /// "Beatmap" + /// + public static LocalisableString BeatmapHeader => new TranslatableString(getKey(@"beatmap_header"), @"Beatmap"); + /// /// "General" /// public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General"); + /// + /// "Audio" + /// + public static LocalisableString AudioHeader => new TranslatableString(getKey(@"audio"), @"Audio"); + + /// + /// "HUD" + /// + public static LocalisableString HUDHeader => new TranslatableString(getKey(@"h_u_d"), @"HUD"); + + /// + /// "Input" + /// + public static LocalisableString InputHeader => new TranslatableString(getKey(@"input"), @"Input"); + + /// + /// "Background" + /// + public static LocalisableString BackgroundHeader => new TranslatableString(getKey(@"background"), @"Background"); + /// /// "Background dim" /// diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs index 70d469ec9790..6d5e918aef81 100644 --- a/osu.Game/Localisation/GraphicsSettingsStrings.cs +++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs @@ -104,6 +104,11 @@ public static class GraphicsSettingsStrings /// public static LocalisableString HitLighting => new TranslatableString(getKey(@"hit_lighting"), @"击打闪光"); + /// + /// "Screenshots" + /// + public static LocalisableString Screenshots => new TranslatableString(getKey(@"screenshots"), @"Screenshots"); + /// /// "Screenshot format" /// diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs new file mode 100644 index 000000000000..a356c9e20bed --- /dev/null +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class RulesetSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.RulesetSettings"; + + /// + /// "Rulesets" + /// + public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs index 1954ef53fc2f..2e3278ab7cf4 100644 --- a/osu.Game/Localisation/SkinSettingsStrings.cs +++ b/osu.Game/Localisation/SkinSettingsStrings.cs @@ -14,6 +14,11 @@ public static class SkinSettingsStrings /// public static LocalisableString SkinSectionHeader => new TranslatableString(getKey(@"skin_section_header"), @"Skin"); + /// + /// "Current skin" + /// + public static LocalisableString CurrentSkin => new TranslatableString(getKey(@"current_skin"), @"Current skin"); + /// /// "Skin layout editor" /// diff --git a/osu.Game/Models/RealmBeatmap.cs b/osu.Game/Models/RealmBeatmap.cs new file mode 100644 index 000000000000..5049c1384d3d --- /dev/null +++ b/osu.Game/Models/RealmBeatmap.cs @@ -0,0 +1,117 @@ +// 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 JetBrains.Annotations; +using Newtonsoft.Json; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Rulesets; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + /// + /// A single beatmap difficulty. + /// + [ExcludeFromDynamicCompile] + [Serializable] + [MapTo("Beatmap")] + public class RealmBeatmap : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo + { + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + public string DifficultyName { get; set; } = string.Empty; + + public RealmRuleset Ruleset { get; set; } = null!; + + public RealmBeatmapDifficulty Difficulty { get; set; } = null!; + + public RealmBeatmapMetadata Metadata { get; set; } = null!; + + public RealmBeatmapSet? BeatmapSet { get; set; } + + public BeatmapSetOnlineStatus Status + { + get => (BeatmapSetOnlineStatus)StatusInt; + set => StatusInt = (int)value; + } + + [MapTo(nameof(Status))] + public int StatusInt { get; set; } + + public int? OnlineID { get; set; } + + public double Length { get; set; } + + public double BPM { get; set; } + + public string Hash { get; set; } = string.Empty; + + public double StarRating { get; set; } + + public string MD5Hash { get; set; } = string.Empty; + + [JsonIgnore] + public bool Hidden { get; set; } + + public RealmBeatmap(RealmRuleset ruleset, RealmBeatmapDifficulty difficulty, RealmBeatmapMetadata metadata) + { + Ruleset = ruleset; + Difficulty = difficulty; + Metadata = metadata; + } + + [UsedImplicitly] + private RealmBeatmap() + { + } + + #region Properties we may not want persisted (but also maybe no harm?) + + public double AudioLeadIn { get; set; } + + public float StackLeniency { get; set; } = 0.7f; + + public bool SpecialStyle { get; set; } + + public bool LetterboxInBreaks { get; set; } + + public bool WidescreenStoryboard { get; set; } + + public bool EpilepsyWarning { get; set; } + + public bool SamplesMatchPlaybackRate { get; set; } + + public double DistanceSpacing { get; set; } + + public int BeatDivisor { get; set; } + + public int GridSize { get; set; } + + public double TimelineZoom { get; set; } + + #endregion + + public bool AudioEquals(RealmBeatmap? other) => other != null + && BeatmapSet != null + && other.BeatmapSet != null + && BeatmapSet.Hash == other.BeatmapSet.Hash + && Metadata.AudioFile == other.Metadata.AudioFile; + + public bool BackgroundEquals(RealmBeatmap? other) => other != null + && BeatmapSet != null + && other.BeatmapSet != null + && BeatmapSet.Hash == other.BeatmapSet.Hash + && Metadata.BackgroundFile == other.Metadata.BackgroundFile; + + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; + IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; + IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; + IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => Difficulty; + } +} diff --git a/osu.Game/Models/RealmBeatmapDifficulty.cs b/osu.Game/Models/RealmBeatmapDifficulty.cs new file mode 100644 index 000000000000..3c1dad69e4a8 --- /dev/null +++ b/osu.Game/Models/RealmBeatmapDifficulty.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("BeatmapDifficulty")] + public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo + { + public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float ApproachRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + + public double SliderMultiplier { get; set; } = 1; + public double SliderTickRate { get; set; } = 1; + + /// + /// Returns a shallow-clone of this . + /// + public RealmBeatmapDifficulty Clone() + { + var diff = new RealmBeatmapDifficulty(); + CopyTo(diff); + return diff; + } + + public void CopyTo(RealmBeatmapDifficulty difficulty) + { + difficulty.ApproachRate = ApproachRate; + difficulty.DrainRate = DrainRate; + difficulty.CircleSize = CircleSize; + difficulty.OverallDifficulty = OverallDifficulty; + + difficulty.SliderMultiplier = SliderMultiplier; + difficulty.SliderTickRate = SliderTickRate; + } + } +} diff --git a/osu.Game/Models/RealmBeatmapMetadata.cs b/osu.Game/Models/RealmBeatmapMetadata.cs new file mode 100644 index 000000000000..6ea7170d0f90 --- /dev/null +++ b/osu.Game/Models/RealmBeatmapMetadata.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using Newtonsoft.Json; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [Serializable] + [MapTo("BeatmapMetadata")] + public class RealmBeatmapMetadata : RealmObject, IBeatmapMetadataInfo + { + public string Title { get; set; } = string.Empty; + + [JsonProperty("title_unicode")] + public string TitleUnicode { get; set; } = string.Empty; + + public string Artist { get; set; } = string.Empty; + + [JsonProperty("artist_unicode")] + public string ArtistUnicode { get; set; } = string.Empty; + + public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User. + + public string Source { get; set; } = string.Empty; + + [JsonProperty(@"tags")] + public string Tags { get; set; } = string.Empty; + + /// + /// The time in milliseconds to begin playing the track for preview purposes. + /// If -1, the track should begin playing at 40% of its length. + /// + public int PreviewTime { get; set; } + + public string AudioFile { get; set; } = string.Empty; + public string BackgroundFile { get; set; } = string.Empty; + } +} diff --git a/osu.Game/Models/RealmBeatmapSet.cs b/osu.Game/Models/RealmBeatmapSet.cs new file mode 100644 index 000000000000..314ca4494b71 --- /dev/null +++ b/osu.Game/Models/RealmBeatmapSet.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("BeatmapSet")] + public class RealmBeatmapSet : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IBeatmapSetInfo + { + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + public int? OnlineID { get; set; } + + public DateTimeOffset DateAdded { get; set; } + + public IBeatmapMetadataInfo? Metadata => Beatmaps.FirstOrDefault()?.Metadata; + + public IList Beatmaps { get; } = null!; + + public IList Files { get; } = null!; + + public bool DeletePending { get; set; } + + public string Hash { get; set; } = string.Empty; + + /// + /// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present). + /// + public bool Protected { get; set; } + + public double MaxStarDifficulty => Beatmaps.Max(b => b.StarRating); + + public double MaxLength => Beatmaps.Max(b => b.Length); + + public double MaxBPM => Beatmaps.Max(b => b.BPM); + + /// + /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null. + /// The path returned is relative to the user file storage. + /// + /// The name of the file to get the storage path of. + public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.StoragePath; + + public override string ToString() => Metadata?.ToString() ?? base.ToString(); + + public bool Equals(RealmBeatmapSet? other) + { + if (other == null) + return false; + + if (IsManaged && other.IsManaged) + return ID == other.ID; + + if (OnlineID.HasValue && other.OnlineID.HasValue) + return OnlineID == other.OnlineID; + + if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) + return Hash == other.Hash; + + return ReferenceEquals(this, other); + } + + IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; + + IEnumerable IBeatmapSetInfo.Files => Files; + } +} diff --git a/osu.Game/Models/RealmFile.cs b/osu.Game/Models/RealmFile.cs new file mode 100644 index 000000000000..2715f4be4562 --- /dev/null +++ b/osu.Game/Models/RealmFile.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using osu.Framework.Testing; +using osu.Game.IO; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("File")] + public class RealmFile : RealmObject, IFileInfo + { + [PrimaryKey] + public string Hash { get; set; } = string.Empty; + + public string StoragePath => Path.Combine(Hash.Remove(1), Hash.Remove(2), Hash); + } +} diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs new file mode 100644 index 000000000000..ba12d51d0b33 --- /dev/null +++ b/osu.Game/Models/RealmNamedFileUsage.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Testing; +using osu.Game.Database; +using osu.Game.IO; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + public class RealmNamedFileUsage : EmbeddedObject, INamedFile, INamedFileUsage + { + public RealmFile File { get; set; } = null!; + + public string Filename { get; set; } = null!; + + public RealmNamedFileUsage(RealmFile file, string filename) + { + File = file; + Filename = filename; + } + + [UsedImplicitly] + private RealmNamedFileUsage() + { + } + + IFileInfo INamedFileUsage.File => File; + } +} diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Models/RealmRuleset.cs new file mode 100644 index 000000000000..0dcd701ed235 --- /dev/null +++ b/osu.Game/Models/RealmRuleset.cs @@ -0,0 +1,63 @@ +// 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 JetBrains.Annotations; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("Ruleset")] + public class RealmRuleset : RealmObject, IEquatable, IRulesetInfo + { + [PrimaryKey] + public string ShortName { get; set; } = string.Empty; + + public int? OnlineID { get; set; } + + public string Name { get; set; } = string.Empty; + + public string InstantiationInfo { get; set; } = string.Empty; + + public RealmRuleset(string shortName, string name, string instantiationInfo, int? onlineID = null) + { + ShortName = shortName; + Name = name; + InstantiationInfo = instantiationInfo; + OnlineID = onlineID; + } + + [UsedImplicitly] + private RealmRuleset() + { + } + + public RealmRuleset(int? onlineID, string name, string shortName, bool available) + { + OnlineID = onlineID; + Name = name; + ShortName = shortName; + Available = available; + } + + public bool Available { get; set; } + + public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + + public override string ToString() => Name; + + public RealmRuleset Clone() => new RealmRuleset + { + OnlineID = OnlineID, + Name = Name, + ShortName = ShortName, + InstantiationInfo = InstantiationInfo, + Available = Available + }; + } +} diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 4b71ebdc3f69..4d843eed8aef 100755 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -180,6 +180,24 @@ public static LinkDetails GetLinkDetails(string url) case "wiki": return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3))); + + case "home": + if (mainArg != "changelog") + // handle link other than changelog as external for now + return new LinkDetails(LinkAction.External, url); + + switch (args.Length) + { + case 4: + // https://osu.ppy.sh/home/changelog + return new LinkDetails(LinkAction.OpenChangelog, string.Empty); + + case 6: + // https://osu.ppy.sh/home/changelog/lazer/2021.1006 + return new LinkDetails(LinkAction.OpenChangelog, $"{args[4]}/{args[5]}"); + } + + break; } } @@ -327,7 +345,8 @@ public enum LinkAction SearchBeatmapSet, OpenWiki, Custom, - OpenPictureURL + OpenPictureURL, + OpenChangelog, } public class Link : IComparable diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 75bbaec0ef9e..28505f6b0e31 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -374,7 +374,7 @@ async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user) UserJoined?.Invoke(user); RoomUpdated?.Invoke(); - }, false); + }); } Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) => diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 5f71b4be4a71..39fc7f1da88e 100755 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -130,12 +130,6 @@ private int? maxAttempts set => MaxAttempts.Value = value; } - /// - /// The position of this in the list. This is not read from or written to the API. - /// - [JsonIgnore] - public readonly Bindable Position = new Bindable(-1); // Todo: This does not need to exist. - public Room() { Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue)); @@ -192,8 +186,6 @@ public void CopyFrom(Room other) RecentParticipants.Clear(); RecentParticipants.AddRange(other.RecentParticipants); } - - Position.Value = other.Position.Value; } public void RemoveExpiredPlaylistItems() diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 251fce29c70d..3097b0e90bd7 100755 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -98,6 +98,8 @@ public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUser private WikiOverlay wikiOverlay; + private ChangelogOverlay changelogOverlay; + private SkinEditorOverlay skinEditor; private Container overlayContent; @@ -275,13 +277,6 @@ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnl [BackgroundDependencyLoader] private void load() { - if (args?.Length > 0) - { - var paths = args.Where(a => !a.StartsWith('-')).ToArray(); - if (paths.Length > 0) - Task.Run(() => Import(paths)); - } - dependencies.CacheAs(this); dependencies.Cache(SentryLogger); @@ -410,6 +405,17 @@ public void HandleLink(LinkDetails link) => Schedule(() => ShowWiki(link.Argument); break; + case LinkAction.OpenChangelog: + if (string.IsNullOrEmpty(link.Argument)) + ShowChangelogListing(); + else + { + var changelogArgs = link.Argument.Split("/"); + ShowChangelogBuild(changelogArgs[0], changelogArgs[1]); + } + + break; + default: throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action."); } @@ -475,6 +481,18 @@ public void ShowChannel(string channel) => waitForReady(() => channelManager, _ /// The wiki page to show public void ShowWiki(string path) => waitForReady(() => wikiOverlay, _ => wikiOverlay.ShowPage(path)); + /// + /// Show changelog listing overlay + /// + public void ShowChangelogListing() => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowListing()); + + /// + /// Show changelog's build as an overlay + /// + /// The update stream name + /// The build version of the update stream + public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version)); + /// /// Present a beatmap at song select immediately. /// The user should have already requested this interactively. @@ -610,6 +628,7 @@ private void beatmapChanged(ValueChangedEvent beatmap) { beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); + Logger.Log($"Game-wide working beatmap updated to {beatmap.NewValue}"); } private void modsChanged(ValueChangedEvent> mods) @@ -698,7 +717,7 @@ protected override void LoadComplete() SkinManager.PostNotification = n => Notifications.Post(n); BeatmapManager.PostNotification = n => Notifications.Post(n); - BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value); + BeatmapManager.PostImport = items => PresentBeatmap(items.First().Value); ScoreManager.PostNotification = n => Notifications.Post(n); ScoreManager.PostImport = items => PresentScore(items.First().Value); @@ -861,7 +880,7 @@ protected override void LoadComplete() loadComponentSingleFile(Picture = new OnlinePictureOverlay(), overlayContent.Add, true); loadComponentSingleFile(new MessageNotifier(), AddInternal, true); loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true); - var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true); + loadComponentSingleFile(changelogOverlay = new ChangelogOverlay(), overlayContent.Add, true); loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true); loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true); @@ -934,6 +953,19 @@ protected override void LoadComplete() { if (mode.NewValue != OverlayActivation.All) CloseAllOverlays(); }; + + // Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup. + handleStartupImport(); + } + + private void handleStartupImport() + { + if (args?.Length > 0) + { + var paths = args.Where(a => !a.StartsWith('-')).ToArray(); + if (paths.Length > 0) + Task.Run(() => Import(paths)); + } } private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index c0b55126df6f..8bfaed8c729f 100755 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -190,8 +191,6 @@ private void load() dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client")); - AddInternal(realmFactory); - dependencies.CacheAs(Storage); var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); @@ -429,11 +428,28 @@ public void Migrate(string path) { Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""..."); - using (realmFactory.BlockAllOperations()) + IDisposable realmBlocker = null; + + try { - contextFactory.FlushConnections(); + ManualResetEventSlim readyToRun = new ManualResetEventSlim(); + + Scheduler.Add(() => + { + realmBlocker = realmFactory.BlockAllOperations(); + contextFactory.FlushConnections(); + + readyToRun.Set(); + }, false); + + readyToRun.Wait(); + (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); } + finally + { + realmBlocker?.Dispose(); + } Logger.Log(@"Migration complete!"); } @@ -530,6 +546,7 @@ protected override void Dispose(bool isDisposing) LocalConfig?.Dispose(); contextFactory?.FlushConnections(); + realmFactory?.Dispose(); } } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 935a89b99b58..a7c59ee4290e 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -335,5 +335,11 @@ protected override void Update() if (shouldShowMore) filterControl.FetchNextPage(); } + + protected override void PopIn() + { + base.PopIn(); + Schedule(filterControl.TakeFocus); + } } } diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index f051e09c08d5..9db0f34d1b36 100755 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -7,7 +7,10 @@ using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Input.Events; +using osu.Game.Audio.Effects; namespace osu.Game.Overlays { @@ -18,6 +21,8 @@ public class DialogOverlay : OsuFocusedOverlayContainer protected override string PopInSampleName => "UI/dialog-pop-in"; protected override string PopOutSampleName => "UI/dialog-pop-out"; + private AudioFilter lowPassFilter; + public PopupDialog CurrentDialog { get; private set; } public DialogOverlay() @@ -34,6 +39,12 @@ public DialogOverlay() Origin = Anchor.BottomCentre; } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + AddInternal(lowPassFilter = new AudioFilter(audio.TrackMixer)); + } + public void Push(PopupDialog dialog) { if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return; @@ -71,12 +82,15 @@ protected override void PopIn() { base.PopIn(); this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint); + lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); } protected override void PopOut() { base.PopOut(); + lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); + if (CurrentDialog?.State.Value == Visibility.Visible) { CurrentDialog.Hide(); diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 3105ecd742b3..f8cd31f1934c 100755 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -31,10 +31,12 @@ public float Progress set { progress = value; - Scheduler.AddOnce(() => progressBar.Progress = progress); + Scheduler.AddOnce(updateProgress, progress); } } + private void updateProgress(float progress) => progressBar.Progress = progress; + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs index 51214fe460b5..198aa1438a67 100755 --- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -29,7 +29,7 @@ public class TrackedSettingToast : Toast private Sample sampleChange; public TrackedSettingToast(SettingDescription description) - : base(description.Name, description.Value, description.Shortcut) + : base(description.Name.ToString(), description.Value.ToString(), description.Shortcut.ToString()) { FillFlowContainer optionLights; diff --git a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs index c02db40ecaee..4ca3ace8a136 100755 --- a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs +++ b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs @@ -14,10 +14,7 @@ public class DangerousSettingsButton : SettingsButton [BackgroundDependencyLoader] private void load(OsuColour colours) { - BackgroundColour = colours.Pink; - - Triangles.ColourDark = colours.PinkDark; - Triangles.ColourLight = colours.PinkLight; + BackgroundColour = colours.Pink3; } } } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index d697b4542415..0c54ae2763d9 100755 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -28,6 +28,7 @@ private void load() { dropdown = new AudioDeviceSettingsDropdown { + LabelText = AudioSettingsStrings.OutputDevice, Keywords = new[] { "speaker", "headphone", "output" } } }; diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs new file mode 100644 index 000000000000..dba64d695a04 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class AudioSettings : SettingsSubsection + { + protected override LocalisableString Header => GameplaySettingsStrings.AudioHeader; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.PositionalHitsounds, + Current = config.GetBindable(OsuSetting.PositionalHitSounds) + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak, + Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak) + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs new file mode 100644 index 000000000000..94e0c5e4942a --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class BackgroundSettings : SettingsSubsection + { + protected override LocalisableString Header => GameplaySettingsStrings.BackgroundHeader; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsSlider + { + LabelText = GameplaySettingsStrings.BackgroundDim, + Current = config.GetBindable(OsuSetting.DimLevel), + KeyboardStep = 0.01f, + DisplayAsPercentage = true + }, + new SettingsSlider + { + LabelText = GameplaySettingsStrings.BackgroundBlur, + Current = config.GetBindable(OsuSetting.BlurLevel), + KeyboardStep = 0.01f, + DisplayAsPercentage = true + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.LightenDuringBreaks, + Current = config.GetBindable(OsuSetting.LightenDuringBreaks) + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.FadePlayfieldWhenHealthLow, + Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs new file mode 100644 index 000000000000..aaa60ce81b65 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class BeatmapSettings : SettingsSubsection + { + protected override LocalisableString Header => GameplaySettingsStrings.BeatmapHeader; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = SkinSettingsStrings.BeatmapSkins, + Current = config.GetBindable(OsuSetting.BeatmapSkins) + }, + new SettingsCheckbox + { + LabelText = SkinSettingsStrings.BeatmapColours, + Current = config.GetBindable(OsuSetting.BeatmapColours) + }, + new SettingsCheckbox + { + LabelText = SkinSettingsStrings.BeatmapHitsounds, + Current = config.GetBindable(OsuSetting.BeatmapHitsounds) + }, + new SettingsCheckbox + { + LabelText = GraphicsSettingsStrings.StoryboardVideo, + Current = config.GetBindable(OsuSetting.ShowStoryboard) + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 3a0265e453d8..d4e4fd571d82 100755 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -1,7 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -20,77 +19,18 @@ private void load(OsuConfigManager config) { Children = new Drawable[] { - new SettingsSlider - { - LabelText = GameplaySettingsStrings.BackgroundDim, - Current = config.GetBindable(OsuSetting.DimLevel), - KeyboardStep = 0.01f, - DisplayAsPercentage = true - }, - new SettingsSlider - { - LabelText = GameplaySettingsStrings.BackgroundBlur, - Current = config.GetBindable(OsuSetting.BlurLevel), - KeyboardStep = 0.01f, - DisplayAsPercentage = true - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.LightenDuringBreaks, - Current = config.GetBindable(OsuSetting.LightenDuringBreaks) - }, - new SettingsEnumDropdown - { - LabelText = GameplaySettingsStrings.HUDVisibilityMode, - Current = config.GetBindable(OsuSetting.HUDVisibilityMode) - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.ShowDifficultyGraph, - Current = config.GetBindable(OsuSetting.ShowProgressGraph) - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, - Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), - Keywords = new[] { "hp", "bar" } - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.FadePlayfieldWhenHealthLow, - Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay, - Current = config.GetBindable(OsuSetting.KeyOverlay) - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.PositionalHitsounds, - Current = config.GetBindable(OsuSetting.PositionalHitSounds) - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak, - Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak) - }, new SettingsEnumDropdown { LabelText = GameplaySettingsStrings.ScoreDisplayMode, Current = config.GetBindable(OsuSetting.ScoreDisplayMode), Keywords = new[] { "scoring" } }, - }; - - if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) - { - Add(new SettingsCheckbox + new SettingsCheckbox { - LabelText = GameplaySettingsStrings.DisableWinKey, - Current = config.GetBindable(OsuSetting.GameplayDisableWinKey) - }); - } + LabelText = GraphicsSettingsStrings.HitLighting, + Current = config.GetBindable(OsuSetting.HitLighting) + }, + }; } } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs new file mode 100644 index 000000000000..e1b452e322fc --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class HUDSettings : SettingsSubsection + { + protected override LocalisableString Header => GameplaySettingsStrings.HUDHeader; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsEnumDropdown + { + LabelText = GameplaySettingsStrings.HUDVisibilityMode, + Current = config.GetBindable(OsuSetting.HUDVisibilityMode) + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.ShowDifficultyGraph, + Current = config.GetBindable(OsuSetting.ShowProgressGraph) + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, + Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), + Keywords = new[] { "hp", "bar" } + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay, + Current = config.GetBindable(OsuSetting.KeyOverlay) + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs new file mode 100644 index 000000000000..962572ca6e7d --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class InputSettings : SettingsSubsection + { + protected override LocalisableString Header => GameplaySettingsStrings.InputHeader; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsSlider + { + LabelText = SkinSettingsStrings.GameplayCursorSize, + Current = config.GetBindable(OsuSetting.GameplayCursorSize), + KeyboardStep = 0.01f + }, + new SettingsCheckbox + { + LabelText = SkinSettingsStrings.AutoCursorSize, + Current = config.GetBindable(OsuSetting.AutoCursorSize) + }, + }; + + if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) + { + Add(new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.DisableWinKey, + Current = config.GetBindable(OsuSetting.GameplayDisableWinKey) + }); + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index 42d9d48d7381..120e2d908c5a 100755 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -1,16 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Overlays.Settings.Sections.Gameplay; -using osu.Game.Rulesets; -using System.Linq; using osu.Framework.Graphics.Sprites; -using osu.Framework.Logging; using osu.Framework.Localisation; using osu.Game.Localisation; +using osu.Game.Overlays.Settings.Sections.Gameplay; namespace osu.Game.Overlays.Settings.Sections { @@ -20,7 +15,7 @@ public class GameplaySection : SettingsSection public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Regular.Circle + Icon = FontAwesome.Regular.DotCircle }; public GameplaySection() @@ -28,27 +23,13 @@ public GameplaySection() Children = new Drawable[] { new GeneralSettings(), + new AudioSettings(), + new BeatmapSettings(), + new BackgroundSettings(), + new HUDSettings(), + new InputSettings(), new ModsSettings(), }; } - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance())) - { - try - { - SettingsSubsection section = ruleset.CreateSettings(); - - if (section != null) - Add(section); - } - catch (Exception e) - { - Logger.Error(e, "Failed to load ruleset settings"); - } - } - } } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs similarity index 67% rename from osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs rename to osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs index 20b1d8d8012d..dbb9ddc1c134 100755 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs @@ -9,25 +9,15 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { - public class DetailSettings : SettingsSubsection + public class ScreenshotSettings : SettingsSubsection { - protected override LocalisableString Header => GraphicsSettingsStrings.DetailSettingsHeader; + protected override LocalisableString Header => GraphicsSettingsStrings.Screenshots; [BackgroundDependencyLoader] private void load(OsuConfigManager config) { Children = new Drawable[] { - new SettingsCheckbox - { - LabelText = GraphicsSettingsStrings.StoryboardVideo, - Current = config.GetBindable(OsuSetting.ShowStoryboard) - }, - new SettingsCheckbox - { - LabelText = GraphicsSettingsStrings.HitLighting, - Current = config.GetBindable(OsuSetting.HitLighting) - }, new SettingsEnumDropdown { LabelText = GraphicsSettingsStrings.ScreenshotFormat, diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index fd0718f9f20c..591848506a4d 100755 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs @@ -22,9 +22,9 @@ public GraphicsSection() { Children = new Drawable[] { - new RendererSettings(), new LayoutSettings(), - new DetailSettings(), + new RendererSettings(), + new ScreenshotSettings(), }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index cf8adf27851b..da789db79a65 100755 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -78,7 +78,7 @@ public KeyBindingRow(object action, List bindings) private RealmContextFactory realmFactory { get; set; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -101,7 +101,7 @@ private void load(OsuColour colours) EdgeEffect = new EdgeEffectParameters { Radius = 2, - Colour = colours.YellowDark.Opacity(0), + Colour = colourProvider.Highlight1.Opacity(0), Type = EdgeEffectType.Shadow, Hollow = true, }, @@ -110,13 +110,12 @@ private void load(OsuColour colours) new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.6f, + Colour = colourProvider.Background5, }, text = new OsuSpriteText { Text = action.GetLocalisableDescription(), - Margin = new MarginPadding(padding), + Margin = new MarginPadding(1.5f * padding), }, buttons = new FillFlowContainer { @@ -405,7 +404,8 @@ public class KeyButton : Container private readonly Box box; public readonly OsuSpriteText Text; - private Color4 hoverColour; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } private bool isBinding; @@ -448,7 +448,6 @@ public KeyButton(RealmKeyBinding keyBinding) box = new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black }, Text = new OsuSpriteText { @@ -463,9 +462,9 @@ public KeyButton(RealmKeyBinding keyBinding) } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - hoverColour = colours.YellowDark; + updateHoverState(); } protected override bool OnHover(HoverEvent e) @@ -484,12 +483,12 @@ private void updateHoverState() { if (isBinding) { - box.FadeColour(Color4.White, transition_time, Easing.OutQuint); + box.FadeColour(colourProvider.Light2, transition_time, Easing.OutQuint); Text.FadeColour(Color4.Black, transition_time, Easing.OutQuint); } else { - box.FadeColour(IsHovered ? hoverColour : Color4.Black, transition_time, Easing.OutQuint); + box.FadeColour(IsHovered ? colourProvider.Light4 : colourProvider.Background6, transition_time, Easing.OutQuint); Text.FadeColour(IsHovered ? Color4.Black : Color4.White, transition_time, Easing.OutQuint); } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 2cc2857e9b6d..2051af6f3cd6 100755 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -7,7 +7,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Game.Database; -using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets; using osu.Game.Localisation; @@ -27,8 +26,7 @@ protected KeyBindingsSubsection(int? variant) { this.variant = variant; - FlowContent.Spacing = new Vector2(0, 1); - FlowContent.Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }; + FlowContent.Spacing = new Vector2(0, 3); } [BackgroundDependencyLoader] @@ -60,7 +58,7 @@ private void load(RealmContextFactory realmFactory) } } - public class ResetButton : DangerousTriangleButton + public class ResetButton : DangerousSettingsButton { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs index 26610628d571..3ef5ce894176 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -8,16 +9,24 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Handlers.Tablet; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Overlays.Settings.Sections.Input { - internal class RotationPresetButtons : FillFlowContainer + internal class RotationPresetButtons : CompositeDrawable { + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + private readonly ITabletHandler tabletHandler; private Bindable rotation; + private readonly RotationButton[] rotationPresets = new RotationButton[preset_count]; + private const int preset_count = 4; private const int height = 50; public RotationPresetButtons(ITabletHandler tabletHandler) @@ -27,18 +36,39 @@ public RotationPresetButtons(ITabletHandler tabletHandler) RelativeSizeAxes = Axes.X; Height = height; - for (int i = 0; i < 360; i += 90) + IEnumerable createColumns(int count) + { + for (int i = 0; i < count; ++i) + { + if (i > 0) + yield return new Dimension(GridSizeMode.Absolute, 10); + + yield return new Dimension(); + } + } + + GridContainer grid; + + InternalChild = grid = new GridContainer { - var presetRotation = i; + RelativeSizeAxes = Axes.Both, + ColumnDimensions = createColumns(preset_count).ToArray() + }; - Add(new RotationButton(i) + grid.Content = new[] { new Drawable[preset_count * 2 - 1] }; + + for (int i = 0; i < preset_count; i++) + { + var rotationValue = i * 90; + + var rotationPreset = new RotationButton(rotationValue) { - RelativeSizeAxes = Axes.X, - Height = height, - Width = 0.25f, - Text = $@"{presetRotation}º", - Action = () => tabletHandler.Rotation.Value = presetRotation, - }); + RelativeSizeAxes = Axes.Both, + Height = 1, + Text = $@"{rotationValue}º", + Action = () => tabletHandler.Rotation.Value = rotationValue, + }; + grid.Content[0][2 * i] = rotationPresets[i] = rotationPreset; } } @@ -49,16 +79,19 @@ protected override void LoadComplete() rotation = tabletHandler.Rotation.GetBoundCopy(); rotation.BindValueChanged(val => { - foreach (var b in Children.OfType()) + foreach (var b in rotationPresets) b.IsSelected = b.Preset == val.NewValue; }, true); } - public class RotationButton : TriangleButton + public class RotationButton : RoundedButton { [Resolved] private OsuColour colours { get; set; } + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + public readonly int Preset; public RotationButton(int preset) @@ -91,18 +124,7 @@ protected override void LoadComplete() 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; - } + BackgroundColour = isSelected ? colours.Blue3 : colourProvider.Background3; } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 9a1251125c2f..f05dbe4e9d63 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -165,7 +165,13 @@ private void load(OsuColour colours) LabelText = TabletSettingsStrings.Rotation, Current = rotation }, - new RotationPresetButtons(tabletHandler), + new RotationPresetButtons(tabletHandler) + { + Padding = new MarginPadding + { + Horizontal = SettingsPanel.CONTENT_MARGINS + } + }, new SettingsSlider { TransferValueOnCommit = true, diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 95bf6656bf17..62f4b82affc5 100755 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -10,7 +10,6 @@ using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; -using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Scoring; using osu.Game.Skinning; @@ -21,15 +20,15 @@ public class GeneralSettings : SettingsSubsection { protected override LocalisableString Header => "整体"; - private TriangleButton importBeatmapsButton; - private TriangleButton importScoresButton; - private TriangleButton importSkinsButton; - private TriangleButton importCollectionsButton; - private TriangleButton deleteBeatmapsButton; - private TriangleButton deleteScoresButton; - private TriangleButton deleteSkinsButton; - private TriangleButton restoreButton; - private TriangleButton undeleteButton; + private SettingsButton importBeatmapsButton; + private SettingsButton importScoresButton; + private SettingsButton importSkinsButton; + private SettingsButton importCollectionsButton; + private SettingsButton deleteBeatmapsButton; + private SettingsButton deleteScoresButton; + private SettingsButton deleteSkinsButton; + private SettingsButton restoreButton; + private SettingsButton undeleteButton; [BackgroundDependencyLoader(permitNulls: true)] private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] StableImportManager stableImportManager, DialogOverlay dialogOverlay) diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs index fa0c06167b2c..9410a87848c1 100755 --- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs @@ -6,7 +6,6 @@ using osu.Framework.Localisation; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Maintenance; -using osuTK; namespace osu.Game.Overlays.Settings.Sections { @@ -21,7 +20,6 @@ public class MaintenanceSection : SettingsSection public MaintenanceSection() { - FlowContent.Spacing = new Vector2(0, 5); Children = new Drawable[] { new GeneralSettings() diff --git a/osu.Game/Overlays/Settings/Sections/Mf/MfSettings.cs b/osu.Game/Overlays/Settings/Sections/Mf/MfSettings.cs index 2e946bf70e5c..1621d37dfeda 100644 --- a/osu.Game/Overlays/Settings/Sections/Mf/MfSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Mf/MfSettings.cs @@ -27,12 +27,6 @@ private void load(MConfigManager config, OsuConfigManager osuConfig, GameHost ho Current = config.GetBindable(MSetting.OptUI) }, new SettingsCheckbox - { - LabelText = "总是隐藏输入指示器", - TooltipText = "如果你的窗口经常无缘无故进入编辑状态,或者只是觉得弹出来烦,那么我建议勾选此项", - Current = config.GetBindable(MSetting.AlwaysHideTextIndicator) - }, - new SettingsCheckbox { LabelText = "启用三角形粒子动画", Current = config.GetBindable(MSetting.TrianglesEnabled) diff --git a/osu.Game/Overlays/Settings/Sections/RulesetSection.cs b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs new file mode 100644 index 000000000000..b9339d5299f9 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Framework.Logging; +using osu.Game.Rulesets; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections +{ + public class RulesetSection : SettingsSection + { + public override LocalisableString Header => RulesetSettingsStrings.Rulesets; + + public override Drawable CreateIcon() => new SpriteIcon + { + Icon = FontAwesome.Solid.Chess + }; + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance())) + { + try + { + SettingsSubsection section = ruleset.CreateSettings(); + + if (section != null) + Add(section); + } + catch (Exception e) + { + Logger.Error(e, "Failed to load ruleset settings"); + } + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index d340b639494e..b770b0da7031 100755 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -16,7 +16,6 @@ using osu.Game.Localisation; using osu.Game.Skinning; using osu.Game.Skinning.Editor; -using osuTK; namespace osu.Game.Overlays.Settings.Sections { @@ -63,43 +62,18 @@ private int firstNonDefaultSkinIndex [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor) { - FlowContent.Spacing = new Vector2(0, 5); - Children = new Drawable[] { - skinDropdown = new SkinSettingsDropdown(), + skinDropdown = new SkinSettingsDropdown + { + LabelText = SkinSettingsStrings.CurrentSkin + }, new SettingsButton { Text = SkinSettingsStrings.SkinLayoutEditor, Action = () => skinEditor?.Toggle(), }, new ExportSkinButton(), - new SettingsSlider - { - LabelText = SkinSettingsStrings.GameplayCursorSize, - Current = config.GetBindable(OsuSetting.GameplayCursorSize), - KeyboardStep = 0.01f - }, - new SettingsCheckbox - { - LabelText = SkinSettingsStrings.AutoCursorSize, - Current = config.GetBindable(OsuSetting.AutoCursorSize) - }, - new SettingsCheckbox - { - LabelText = SkinSettingsStrings.BeatmapSkins, - Current = config.GetBindable(OsuSetting.BeatmapSkins) - }, - new SettingsCheckbox - { - LabelText = SkinSettingsStrings.BeatmapColours, - Current = config.GetBindable(OsuSetting.BeatmapColours) - }, - new SettingsCheckbox - { - LabelText = SkinSettingsStrings.BeatmapHitsounds, - Current = config.GetBindable(OsuSetting.BeatmapHitsounds) - }, }; managerUpdated = skins.ItemUpdated.GetBoundCopy(); diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs index 4d640d5707e7..ed8e217c9a68 100755 --- a/osu.Game/Overlays/Settings/SettingsButton.cs +++ b/osu.Game/Overlays/Settings/SettingsButton.cs @@ -7,11 +7,11 @@ using osu.Framework.Graphics.Cursor; using osu.Game.Graphics; using osu.Framework.Localisation; -using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Overlays.Settings { - public class SettingsButton : TriangleButton, IHasTooltip + public class SettingsButton : RoundedButton, IHasTooltip { public SettingsButton() { diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs index 167061f48573..2ed7d6063a32 100755 --- a/osu.Game/Overlays/Settings/SettingsDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Overlays.Settings { @@ -27,6 +28,11 @@ public IBindableList ItemSource public override IEnumerable FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString())); + public SettingsDropdown() + { + FlowContent.Spacing = new Vector2(0, 10); + } + protected sealed override Drawable CreateControl() => CreateDropdown(); protected virtual OsuDropdown CreateDropdown() => new DropdownControl(); @@ -35,7 +41,6 @@ protected class DropdownControl : OsuDropdown { public DropdownControl() { - Margin = new MarginPadding { Top = 5 }; RelativeSizeAxes = Axes.X; } } diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs index 9987a0c6076d..199ba14b48c7 100755 --- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs @@ -16,7 +16,6 @@ public class SettingsEnumDropdown : SettingsDropdown { public DropdownControl() { - Margin = new MarginPadding { Top = 5 }; RelativeSizeAxes = Axes.X; } diff --git a/osu.Game/Overlays/Settings/SettingsHeader.cs b/osu.Game/Overlays/Settings/SettingsHeader.cs index a7f1cef74c64..69b7b69a2902 100755 --- a/osu.Game/Overlays/Settings/SettingsHeader.cs +++ b/osu.Game/Overlays/Settings/SettingsHeader.cs @@ -22,7 +22,7 @@ public SettingsHeader(LocalisableString heading, LocalisableString subheading) } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -39,7 +39,7 @@ private void load(OsuColour colours) new OsuSpriteText { Text = heading, - Font = OsuFont.GetFont(size: 40), + Font = OsuFont.TorusAlternate.With(size: 40), Margin = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, @@ -48,7 +48,7 @@ private void load(OsuColour colours) }, new OsuSpriteText { - Colour = colours.Pink, + Colour = colourProvider.Content2, Text = subheading, Font = OsuFont.GetFont(size: 18), Margin = new MarginPadding diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index 2fbe5224795e..d36aa2bfc20b 100755 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -36,6 +36,7 @@ public NumberControl() { numberBox = new OutlinedNumberBox { + LengthLimit = 9, // limited to less than a value that could overflow int32 backing. Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, CommitOnFocusLost = true diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index 2a6f3f5ed715..0ae353602e8f 100755 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -12,7 +12,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osuTK.Graphics; +using osuTK; namespace osu.Game.Overlays.Settings { @@ -31,9 +31,10 @@ public abstract class SettingsSection : Container, IHasFilterableChildren public IEnumerable FilterableChildren => Children.OfType(); public virtual IEnumerable FilterTerms => new[] { Header.ToString() }; - private const int header_size = 26; - private const int margin = 20; - private const int border_size = 2; + public const int ITEM_SPACING = 14; + + private const int header_size = 24; + private const int border_size = 4; public bool MatchingFilter { @@ -54,8 +55,9 @@ protected SettingsSection() { Margin = new MarginPadding { - Top = header_size + Top = 36 }, + Spacing = new Vector2(0, ITEM_SPACING), Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, @@ -63,14 +65,14 @@ protected SettingsSection() } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { AddRangeInternal(new Drawable[] { new Box { Name = "separator", - Colour = new Color4(0, 0, 0, 255), + Colour = colourProvider.Background6, RelativeSizeAxes = Axes.X, Height = border_size, }, @@ -78,8 +80,8 @@ private void load(OsuColour colours) { Padding = new MarginPadding { - Top = margin + border_size, - Bottom = margin + 10, + Top = 28, + Bottom = 40, }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -87,13 +89,11 @@ private void load(OsuColour colours) { header = new OsuSpriteText { - Font = OsuFont.GetFont(size: header_size), + Font = OsuFont.TorusAlternate.With(size: header_size), Text = Header, - Colour = colours.Yellow, Margin = new MarginPadding { - Left = SettingsPanel.CONTENT_MARGINS, - Right = SettingsPanel.CONTENT_MARGINS + Horizontal = SettingsPanel.CONTENT_MARGINS } }, FlowContent diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs index 9fc3379b946b..bb9c0dd4d7e5 100755 --- a/osu.Game/Overlays/Settings/SettingsSlider.cs +++ b/osu.Game/Overlays/Settings/SettingsSlider.cs @@ -19,7 +19,7 @@ public class SettingsSlider : SettingsItem { protected override Drawable CreateControl() => new TSlider { - Margin = new MarginPadding { Top = 5, Bottom = 5 }, + Margin = new MarginPadding { Vertical = 10 }, RelativeSizeAxes = Axes.X }; diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index 718346be3660..c2cf08ac9833 100755 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -46,13 +46,17 @@ protected SettingsSubsection() FlowContent = new FillFlowContainer { + Margin = new MarginPadding { Top = SettingsSection.ITEM_SPACING }, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 8), + Spacing = new Vector2(0, SettingsSection.ITEM_SPACING), RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }; } + private const int header_height = 43; + private const int header_font_size = 20; + [BackgroundDependencyLoader] private void load() { @@ -60,9 +64,9 @@ private void load() { new OsuSpriteText { - Text = Header, // TODO: Add localisation support after https://github.com/ppy/osu-framework/pull/4603 is merged. - Margin = new MarginPadding { Vertical = 30, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, - Font = OsuFont.GetFont(weight: FontWeight.Bold), + Text = Header, + Margin = new MarginPadding { Vertical = (header_height - header_font_size) * 0.5f, Horizontal = SettingsPanel.CONTENT_MARGINS }, + Font = OsuFont.GetFont(size: header_font_size), }, FlowContent }); diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index d28dbf106848..68562802cf85 100755 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Framework.Bindables; using osu.Framework.Graphics; namespace osu.Game.Overlays.Settings @@ -13,5 +15,17 @@ public class SettingsTextBox : SettingsItem RelativeSizeAxes = Axes.X, CommitOnFocusLost = true }; + + public override Bindable Current + { + get => base.Current; + set + { + if (value.Default == null) + throw new InvalidOperationException($"Bindable settings of type {nameof(Bindable)} should have a non-null default value."); + + base.Current = value; + } + } } } diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index 4ca6e2ec423e..93b1b19b1719 100755 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -15,22 +16,23 @@ namespace osu.Game.Overlays.Settings { - public class Sidebar : Container, IStateful + public class Sidebar : Container, IStateful { - private readonly FillFlowContainer content; - public const float DEFAULT_WIDTH = Toolbar.Toolbar.HEIGHT * 1.4f; + private readonly Box background; + private readonly FillFlowContainer content; + public const float DEFAULT_WIDTH = 70; public const int EXPANDED_WIDTH = 200; public event Action StateChanged; - protected override Container Content => content; + protected override Container Content => content; public Sidebar() { RelativeSizeAxes = Axes.Y; InternalChildren = new Drawable[] { - new Box + background = new Box { Colour = OsuColour.Gray(0.02f), RelativeSizeAxes = Axes.Both, @@ -39,7 +41,7 @@ public Sidebar() { Children = new[] { - content = new FillFlowContainer + content = new FillFlowContainer { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, @@ -52,6 +54,12 @@ public Sidebar() }; } + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + background.Colour = colourProvider.Background5; + } + private ScheduledDelegate expandEvent; private ExpandedState state; @@ -80,8 +88,6 @@ private class SidebarScrollContainer : OsuScrollContainer { public SidebarScrollContainer() { - Content.Anchor = Anchor.CentreLeft; - Content.Origin = Anchor.CentreLeft; RelativeSizeAxes = Axes.Both; ScrollbarVisible = false; } diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index cf6a313a1fe7..1a34143e1f34 100755 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -1,110 +1,40 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osuTK; -using osuTK.Graphics; using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Settings { - public class SidebarButton : OsuButton + public abstract class SidebarButton : OsuButton { - private readonly ConstrainedIconContainer iconContainer; - private readonly SpriteText headerText; - private readonly Box selectionIndicator; - private readonly Container text; + protected const double FADE_DURATION = 500; - // always consider as part of flow, even when not visible (for the sake of the initial animation). - public override bool IsPresent => true; + [Resolved] + protected OverlayColourProvider ColourProvider { get; private set; } - private SettingsSection section; - - public SettingsSection Section + [BackgroundDependencyLoader] + private void load() { - get => section; - set - { - section = value; - headerText.Text = value.Header; - iconContainer.Icon = value.CreateIcon(); - } + BackgroundColour = ColourProvider.Background5; } - private bool selected; - - public bool Selected + protected override void LoadComplete() { - get => selected; - set - { - selected = value; - - if (selected) - { - selectionIndicator.FadeIn(50); - text.FadeColour(Color4.White, 50); - } - else - { - selectionIndicator.FadeOut(50); - text.FadeColour(OsuColour.Gray(0.6f), 50); - } - } + base.LoadComplete(); + UpdateState(); + FinishTransforms(true); } - public SidebarButton() + protected override bool OnHover(HoverEvent e) { - Height = Sidebar.DEFAULT_WIDTH; - RelativeSizeAxes = Axes.X; - - BackgroundColour = Color4.Black; - - AddRange(new Drawable[] - { - text = new Container - { - Width = Sidebar.DEFAULT_WIDTH, - RelativeSizeAxes = Axes.Y, - Colour = OsuColour.Gray(0.6f), - Children = new Drawable[] - { - headerText = new OsuSpriteText - { - Position = new Vector2(Sidebar.DEFAULT_WIDTH + 10, 0), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - iconContainer = new ConstrainedIconContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(20), - }, - } - }, - selectionIndicator = new Box - { - Alpha = 0, - RelativeSizeAxes = Axes.Y, - Width = 5, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - }, - }); + UpdateState(); + return false; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - selectionIndicator.Colour = colours.Yellow; - } + protected override void OnHoverLost(HoverLostEvent e) => UpdateState(); + + protected abstract void UpdateState(); } } diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs new file mode 100644 index 000000000000..fd57996b1bdf --- /dev/null +++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs @@ -0,0 +1,130 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Overlays.Settings +{ + public class SidebarIconButton : SidebarButton + { + private const float selection_indicator_height_active = 18; + private const float selection_indicator_height_inactive = 4; + + private readonly ConstrainedIconContainer iconContainer; + private readonly SpriteText headerText; + private readonly CircularContainer selectionIndicator; + private readonly Container textIconContent; + + // always consider as part of flow, even when not visible (for the sake of the initial animation). + public override bool IsPresent => true; + + private SettingsSection section; + + public SettingsSection Section + { + get => section; + set + { + section = value; + headerText.Text = value.Header; + iconContainer.Icon = value.CreateIcon(); + } + } + + private bool selected; + + public bool Selected + { + get => selected; + set + { + selected = value; + + if (IsLoaded) + UpdateState(); + } + } + + public SidebarIconButton() + { + RelativeSizeAxes = Axes.X; + Height = 46; + + AddRange(new Drawable[] + { + textIconContent = new Container + { + Width = Sidebar.DEFAULT_WIDTH, + RelativeSizeAxes = Axes.Y, + Colour = OsuColour.Gray(0.6f), + Children = new Drawable[] + { + headerText = new OsuSpriteText + { + Position = new Vector2(Sidebar.DEFAULT_WIDTH + 10, 0), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + iconContainer = new ConstrainedIconContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(20), + }, + } + }, + selectionIndicator = new CircularContainer + { + Alpha = 0, + Width = 4, + Height = selection_indicator_height_inactive, + Masking = true, + CornerRadius = 1.5f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding + { + Left = 9, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White + } + }, + }); + } + + [BackgroundDependencyLoader] + private void load() + { + selectionIndicator.Colour = ColourProvider.Highlight1; + } + + protected override void UpdateState() + { + if (Selected) + { + textIconContent.FadeColour(ColourProvider.Content1, FADE_DURATION, Easing.OutQuint); + + selectionIndicator.FadeIn(FADE_DURATION, Easing.OutQuint); + selectionIndicator.ResizeHeightTo(selection_indicator_height_active, FADE_DURATION, Easing.OutElasticHalf); + } + else + { + textIconContent.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint); + + selectionIndicator.FadeOut(FADE_DURATION, Easing.OutQuint); + selectionIndicator.ResizeHeightTo(selection_indicator_height_inactive, FADE_DURATION, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 220d7baa3cf1..42560c18d6ed 100755 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -25,12 +25,13 @@ public class SettingsOverlay : SettingsPanel, INamedOverlayComponent { new MfSection(createSubPanel(new MfSettingsPanel())), new GeneralSection(), - new GraphicsSection(), - new AudioSection(), + new SkinSection(), new InputSection(createSubPanel(new KeyBindingPanel())), new UserInterfaceSection(), new GameplaySection(), - new SkinSection(), + new RulesetSection(), + new AudioSection(), + new GraphicsSection(), new OnlineSection(), new MaintenanceSection(), new DebugSection(), diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index bda4bb5ece1b..0ceb7fc50dab 100755 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading.Tasks; using osuTK; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; @@ -15,7 +14,6 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; @@ -25,7 +23,7 @@ namespace osu.Game.Overlays [Cached] public abstract class SettingsPanel : OsuFocusedOverlayContainer { - public const float CONTENT_MARGINS = 15; + public const float CONTENT_MARGINS = 20; public const float TRANSITION_LENGTH = 600; @@ -46,7 +44,7 @@ public abstract class SettingsPanel : OsuFocusedOverlayContainer protected override Container Content => ContentContainer; protected Sidebar Sidebar; - private SidebarButton selectedSidebarButton; + private SidebarIconButton selectedSidebarButton; public SettingsSectionsContainer SectionsContainer { get; private set; } @@ -64,6 +62,9 @@ public abstract class SettingsPanel : OsuFocusedOverlayContainer public IBindable CurrentSection = new Bindable(); + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + protected SettingsPanel(bool showSidebar) { this.showSidebar = showSidebar; @@ -89,7 +90,7 @@ private void load() Origin = Anchor.TopRight, Scale = new Vector2(2, 1), // over-extend to the left for transitions RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.05f), + Colour = colourProvider.Background4, Alpha = 1, }, loading = new LoadingLayer @@ -105,17 +106,23 @@ private void load() RelativeSizeAxes = Axes.Both, ExpandableHeader = CreateHeader(), SelectedSection = { BindTarget = CurrentSection }, - FixedHeader = searchTextBox = new SeekLimitedSearchTextBox + FixedHeader = new Container { RelativeSizeAxes = Axes.X, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Width = 0.95f, - Margin = new MarginPadding + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - Top = 20, - Bottom = 20 + Vertical = 20, + Horizontal = CONTENT_MARGINS }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Child = searchTextBox = new SeekLimitedSearchTextBox + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + } }, Footer = CreateFooter().With(f => f.Alpha = 0) }); @@ -245,11 +252,11 @@ private void loadSidebarButtons() }); } - private IEnumerable createSidebarButtons() + private IEnumerable createSidebarButtons() { foreach (var section in SectionsContainer) { - yield return new SidebarButton + yield return new SidebarIconButton { Section = section, Action = () => @@ -292,11 +299,12 @@ protected override FlowContainer CreateScrollContentContainer() Direction = FillDirection.Vertical, }; - public SettingsSectionsContainer() + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { HeaderBackground = new Box { - Colour = Color4.Black, + Colour = colourProvider.Background4, RelativeSizeAxes = Axes.Both }; } diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index 6c268de60ae5..9438999df624 100755 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -7,10 +7,8 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays { @@ -34,18 +32,18 @@ private void load() protected override bool DimMainContent => false; // dimming is handled by main overlay - private class BackButton : OsuButton + private class BackButton : SidebarButton { + private Container content; + [BackgroundDependencyLoader] private void load() { Size = new Vector2(Sidebar.DEFAULT_WIDTH); - BackgroundColour = Color4.Black; - AddRange(new Drawable[] { - new Container + content = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -71,6 +69,11 @@ private void load() } }); } + + protected override void UpdateState() + { + content.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint); + } } } } diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index a0ec8e3e0ef9..eec71a36234f 100755 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -47,9 +47,34 @@ protected override void PerformLoad() } } + private readonly HashSet pendingWrites = new HashSet(); + protected override bool PerformSave() { - // do nothing, realm saves immediately + TLookup[] changed; + + lock (pendingWrites) + { + changed = pendingWrites.ToArray(); + pendingWrites.Clear(); + } + + if (realmFactory == null) + return true; + + using (var context = realmFactory.CreateContext()) + { + context.Write(realm => + { + foreach (var c in changed) + { + var setting = realm.All().First(s => s.RulesetID == rulesetId && s.Variant == variant && s.Key == c.ToString()); + + setting.Value = ConfigStore[c].ToString(); + } + }); + } + return true; } @@ -80,7 +105,8 @@ protected override void AddBindable(TLookup lookup, Bindable { - realmFactory?.Context.Write(() => setting.Value = b.NewValue.ToString()); + lock (pendingWrites) + pendingWrites.Add(lookup); }; } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 1143549f7f5b..200bbf3f929e 100755 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -258,6 +258,13 @@ public ControlPointInfo ControlPointInfo } public BeatmapMetadata Metadata => baseBeatmap.Metadata; + + public BeatmapDifficulty Difficulty + { + get => baseBeatmap.Difficulty; + set => baseBeatmap.Difficulty = value; + } + public List Breaks => baseBeatmap.Breaks; public double TotalBreakTime => baseBeatmap.TotalBreakTime; public IEnumerable GetStatistics() => baseBeatmap.GetStatistics(); diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs index 73bab31e8278..d8babf2f3224 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs @@ -27,7 +27,7 @@ public abstract class StrainDecaySkill : StrainSkill /// /// The current strain level. /// - protected double CurrentStrain { get; private set; } = 1; + protected double CurrentStrain { get; private set; } protected StrainDecaySkill(Mod[] mods) : base(mods) diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 0880f1b08e97..bbd2f079aaae 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -25,7 +25,7 @@ public abstract class StrainSkill : Skill /// protected virtual int SectionLength => 400; - private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section. + private double currentSectionPeak; // We also keep track of the peak strain level in the current section. private double currentSectionEnd; diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 81f480878957..6ed91e983ae0 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -24,6 +24,11 @@ public class BeatmapVerifier : IBeatmapVerifier new CheckAudioQuality(), new CheckMutedObjects(), new CheckFewHitsounds(), + new CheckTooShortAudioFiles(), + new CheckAudioInVideo(), + + // Files + new CheckZeroByteFiles(), // Compose new CheckUnsnappedObjects(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs new file mode 100644 index 000000000000..ac2542beb020 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs @@ -0,0 +1,112 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using osu.Game.IO.FileAbstraction; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Storyboards; +using TagLib; +using File = TagLib.File; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckAudioInVideo : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Audio track in video files"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateHasAudioTrack(this), + new IssueTemplateMissingFile(this), + new IssueTemplateFileError(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + var videoPaths = new List(); + + foreach (var layer in context.WorkingBeatmap.Storyboard.Layers) + { + foreach (var element in layer.Elements) + { + if (!(element is StoryboardVideo video)) + continue; + + // Ensures we don't check the same video file multiple times in case of multiple elements using it. + if (!videoPaths.Contains(video.Path)) + videoPaths.Add(video.Path); + } + } + + foreach (var filename in videoPaths) + { + string storagePath = beatmapSet.GetPathForFile(filename); + + if (storagePath == null) + { + // There's an element in the storyboard that requires this resource, so it being missing is worth warning about. + yield return new IssueTemplateMissingFile(this).Create(filename); + + continue; + } + + Issue issue; + + try + { + // We use TagLib here for platform invariance; BASS cannot detect audio presence on Linux. + using (Stream data = context.WorkingBeatmap.GetStream(storagePath)) + using (File tagFile = File.Create(new StreamFileAbstraction(filename, data))) + { + if (tagFile.Properties.AudioChannels == 0) + continue; + } + + issue = new IssueTemplateHasAudioTrack(this).Create(filename); + } + catch (CorruptFileException) + { + issue = new IssueTemplateFileError(this).Create(filename, "Corrupt file"); + } + catch (UnsupportedFormatException) + { + issue = new IssueTemplateFileError(this).Create(filename, "Unsupported format"); + } + + yield return issue; + } + } + + public class IssueTemplateHasAudioTrack : IssueTemplate + { + public IssueTemplateHasAudioTrack(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" has an audio track.") + { + } + + public Issue Create(string filename) => new Issue(this, filename); + } + + public class IssueTemplateFileError : IssueTemplate + { + public IssueTemplateFileError(ICheck check) + : base(check, IssueType.Error, "Could not check whether \"{0}\" has an audio track ({1}).") + { + } + + public Issue Create(string filename, string errorReason) => new Issue(this, filename, errorReason); + } + + public class IssueTemplateMissingFile : IssueTemplate + { + public IssueTemplateMissingFile(ICheck check) + : base(check, IssueType.Warning, "Could not check whether \"{0}\" has an audio track, because it is missing.") + { + } + + public Issue Create(string filename) => new Issue(this, filename); + } + } +} diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs new file mode 100644 index 000000000000..57f7c609167a --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs @@ -0,0 +1,85 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ManagedBass; +using osu.Framework.Audio.Callbacks; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckTooShortAudioFiles : ICheck + { + private const int ms_threshold = 25; + private const int min_bytes_threshold = 100; + + private readonly string[] audioExtensions = { "mp3", "ogg", "wav" }; + + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateTooShort(this), + new IssueTemplateBadFormat(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + + foreach (var file in beatmapSet.Files) + { + using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.StoragePath)) + { + if (data == null) + continue; + + var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle); + + if (decodeStream == 0) + { + // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it. + // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check. + if (hasAudioExtension(file.Filename) && probablyHasAudioData(data)) + yield return new IssueTemplateBadFormat(this).Create(file.Filename); + + continue; + } + + long length = Bass.ChannelGetLength(decodeStream); + double ms = Bass.ChannelBytes2Seconds(decodeStream, length) * 1000; + + // Extremely short audio files do not play on some soundcards, resulting in nothing being heard in-game for some users. + if (ms > 0 && ms < ms_threshold) + yield return new IssueTemplateTooShort(this).Create(file.Filename, ms); + } + } + } + + private bool hasAudioExtension(string filename) => audioExtensions.Any(filename.ToLower().EndsWith); + private bool probablyHasAudioData(Stream data) => data.Length > min_bytes_threshold; + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" is too short ({1:0} ms), should be at least {2:0} ms.") + { + } + + public Issue Create(string filename, double ms) => new Issue(this, filename, ms, ms_threshold); + } + + public class IssueTemplateBadFormat : IssueTemplate + { + public IssueTemplateBadFormat(ICheck check) + : base(check, IssueType.Error, "Could not check whether \"{0}\" is too short (code \"{1}\").") + { + } + + public Issue Create(string filename) => new Issue(this, filename, Bass.LastError); + } + } +} diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs new file mode 100644 index 000000000000..3a994fabfa0a --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckZeroByteFiles : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Files, "Zero-byte files"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateZeroBytes(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + + foreach (var file in beatmapSet.Files) + { + using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.StoragePath)) + { + if (data?.Length == 0) + yield return new IssueTemplateZeroBytes(this).Create(file.Filename); + } + } + } + + public class IssueTemplateZeroBytes : IssueTemplate + { + public IssueTemplateZeroBytes(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" is a 0-byte file.") + { + } + + public Issue Create(string filename) => new Issue(this, filename); + } + } +} diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index fc2541d56cb0..ea925192c144 100755 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -13,7 +13,6 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -389,41 +388,42 @@ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpac return new SnapResult(screenSpacePosition, targetTime, playfield); } - public override float GetBeatSnapDistanceAt(double referenceTime) + public override float GetBeatSnapDistanceAt(HitObject referenceObject) { - DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime); - return (float)(100 * EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / BeatSnapProvider.BeatDivisor); + return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * referenceObject.DifficultyControlPoint.SliderVelocity / BeatSnapProvider.BeatDivisor); } - public override float DurationToDistance(double referenceTime, double duration) + public override float DurationToDistance(HitObject referenceObject, double duration) { - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime); - return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime)); + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); + return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); } - public override double DistanceToDuration(double referenceTime, float distance) + public override double DistanceToDuration(HitObject referenceObject, float distance) { - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime); - return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength; + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); + return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; } - public override double GetSnappedDurationFromDistance(double referenceTime, float distance) - => BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime; + public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) + => BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; - public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) + public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) { - double actualDuration = referenceTime + DistanceToDuration(referenceTime, distance); + double startTime = referenceObject.StartTime; - double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, referenceTime); + double actualDuration = startTime + DistanceToDuration(referenceObject, distance); - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime); + double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, startTime); + + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(startTime); // we don't want to exceed the actual duration and snap to a point in the future. // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. if (snappedEndTime > actualDuration + 1) snappedEndTime -= beatLength; - return DurationToDistance(referenceTime, snappedEndTime - referenceTime); + return DurationToDistance(referenceObject, snappedEndTime - startTime); } #endregion @@ -466,15 +466,15 @@ protected HitObjectComposer() public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, null); - public abstract float GetBeatSnapDistanceAt(double referenceTime); + public abstract float GetBeatSnapDistanceAt(HitObject referenceObject); - public abstract float DurationToDistance(double referenceTime, double duration); + public abstract float DurationToDistance(HitObject referenceObject, double duration); - public abstract double DistanceToDuration(double referenceTime, float distance); + public abstract double DistanceToDuration(HitObject referenceObject, float distance); - public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance); + public abstract double GetSnappedDurationFromDistance(HitObject referenceObject, float distance); - public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance); + public abstract float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance); #endregion } diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index 4664f3808c19..743a2f41fcd2 100755 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.Edit @@ -27,41 +28,41 @@ public interface IPositionSnapProvider /// /// Retrieves the distance between two points within a timing point that are one beat length apart. /// - /// The time of the timing point. + /// An object to be used as a reference point for this operation. /// The distance between two points residing in the timing point that are one beat length apart. - float GetBeatSnapDistanceAt(double referenceTime); + float GetBeatSnapDistanceAt(HitObject referenceObject); /// /// Converts a duration to a distance. /// - /// The time of the timing point which resides in. + /// An object to be used as a reference point for this operation. /// The duration to convert. /// A value that represents as a distance in the timing point. - float DurationToDistance(double referenceTime, double duration); + float DurationToDistance(HitObject referenceObject, double duration); /// /// Converts a distance to a duration. /// - /// The time of the timing point which resides in. + /// An object to be used as a reference point for this operation. /// The distance to convert. /// A value that represents as a duration in the timing point. - double DistanceToDuration(double referenceTime, float distance); + double DistanceToDuration(HitObject referenceObject, float distance); /// /// Converts a distance to a snapped duration. /// - /// The time of the timing point which resides in. + /// An object to be used as a reference point for this operation. /// The distance to convert. /// A value that represents as a duration snapped to the closest beat of the timing point. - double GetSnappedDurationFromDistance(double referenceTime, float distance); + double GetSnappedDurationFromDistance(HitObject referenceObject, float distance); /// /// Converts an unsnapped distance to a snapped distance. /// The returned distance will always be floored (as to never exceed the provided . /// - /// The time of the timing point which resides in. + /// An object to be used as a reference point for this operation. /// The distance to convert. /// A value that represents snapped to the closest beat of the timing point. - float GetSnappedDistanceFromDistance(double referenceTime, float distance); + float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance); } } diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index b7529f39ca14..0c0c5990d1f2 100755 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -112,7 +112,7 @@ public virtual void UpdateTimeAndPosition(SnapResult result) /// Invokes , /// refreshing and parameters for the . /// - protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty); + protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) ?? false; diff --git a/osu.Game/Rulesets/Mods/DifficultyBindable.cs b/osu.Game/Rulesets/Mods/DifficultyBindable.cs index e4304795f264..6cfae0b08560 100644 --- a/osu.Game/Rulesets/Mods/DifficultyBindable.cs +++ b/osu.Game/Rulesets/Mods/DifficultyBindable.cs @@ -27,7 +27,7 @@ public class DifficultyBindable : Bindable /// /// A function that can extract the current value of this setting from a beatmap difficulty for display purposes. /// - public Func ReadCurrentFromDifficulty; + public Func ReadCurrentFromDifficulty; public float Precision { diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 0b159819d48e..035ebe10cb9f 100755 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -67,7 +67,8 @@ public IList Samples } } - public SampleControlPoint SampleControlPoint; + public SampleControlPoint SampleControlPoint = SampleControlPoint.DEFAULT; + public DifficultyControlPoint DifficultyControlPoint = DifficultyControlPoint.DEFAULT; /// /// Whether this is in Kiai time. @@ -94,6 +95,12 @@ public HitObject() foreach (var nested in nestedHitObjects) nested.StartTime += offset; + + if (DifficultyControlPoint != DifficultyControlPoint.DEFAULT) + DifficultyControlPoint.Time = time.NewValue; + + if (SampleControlPoint != SampleControlPoint.DEFAULT) + SampleControlPoint.Time = this.GetEndTime() + control_point_leniency; }; } @@ -105,16 +112,21 @@ public HitObject() /// The cancellation token. public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default) { - ApplyDefaultsToSelf(controlPointInfo, difficulty); + var legacyInfo = controlPointInfo as LegacyControlPointInfo; - if (controlPointInfo is LegacyControlPointInfo legacyInfo) + if (legacyInfo != null) { - // This is done here since ApplyDefaultsToSelf may be used to determine the end time - SampleControlPoint = legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency); + DifficultyControlPoint = (DifficultyControlPoint)legacyInfo.DifficultyPointAt(StartTime).DeepClone(); + DifficultyControlPoint.Time = StartTime; } - else + + ApplyDefaultsToSelf(controlPointInfo, difficulty); + + // This is done here after ApplyDefaultsToSelf as we may require custom defaults to be applied to have an accurate end time. + if (legacyInfo != null) { - SampleControlPoint ??= SampleControlPoint.DEFAULT; + SampleControlPoint = (SampleControlPoint)legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency).DeepClone(); + SampleControlPoint.Time = this.GetEndTime() + control_point_leniency; } nestedHitObjects.Clear(); diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index e1de82ade714..ad191f7ff51d 100755 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -43,9 +43,8 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); - double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; + double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; } diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 85693abb9397..dfeb6b47883c 100755 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -100,7 +100,7 @@ public override void ApplyBeatmap(IBeatmap beatmap) .First() ))); - targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); + targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, min_health_target, mid_health_target, max_health_target); // Add back a portion of the amount of HP to be drained, depending on the lenience requested. targetMinimumHealth += drainLenience * (1 - targetMinimumHealth); diff --git a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs index dcd2cc8b5537..23325bcd13a7 100755 --- a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs +++ b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs @@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Timing { /// - /// A control point which adds an aggregated multiplier based on the provided 's BeatLength and 's SpeedMultiplier. + /// A control point which adds an aggregated multiplier based on the provided 's BeatLength and 's SpeedMultiplier. /// public class MultiplierControlPoint : IComparable { @@ -19,7 +19,7 @@ public class MultiplierControlPoint : IComparable /// /// The aggregate multiplier which this provides. /// - public double Multiplier => Velocity * DifficultyPoint.SpeedMultiplier * BaseBeatLength / TimingPoint.BeatLength; + public double Multiplier => Velocity * EffectPoint.ScrollSpeed * BaseBeatLength / TimingPoint.BeatLength; /// /// The base beat length to scale the provided multiplier relative to. @@ -38,9 +38,9 @@ public class MultiplierControlPoint : IComparable public TimingControlPoint TimingPoint = new TimingControlPoint(); /// - /// The that provides additional difficulty information for this . + /// The that provides additional difficulty information for this . /// - public DifficultyControlPoint DifficultyPoint = new DifficultyControlPoint(); + public EffectControlPoint EffectPoint = new EffectControlPoint(); /// /// Creates a . This is required for JSON serialization diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index e9865f6c8b79..c0b339a2316a 100755 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -55,7 +55,10 @@ public FrameStabilityContainer(double gameplayStartTime = double.MinValue) /// /// The current direction of playback to be exposed to frame stable children. /// - private int direction; + /// + /// Initially it is presumed that playback will proceed in the forward direction. + /// + private int direction = 1; [BackgroundDependencyLoader(true)] private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler) @@ -139,7 +142,9 @@ private void updateClock() state = PlaybackState.NotValid; } - if (state == PlaybackState.Valid) + // if the proposed time is the same as the current time, assume that the clock will continue progressing in the same direction as previously. + // this avoids spurious flips in direction from -1 to 1 during rewinds. + if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime) direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime); diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index 7b30bb9574ff..2a9d3d1cf01c 100755 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -133,32 +133,39 @@ private void load() maxDuration = duration; // The slider multiplier is post-multiplied to determine the final velocity, but for relative scale beat lengths // the multiplier should not affect the effective timing point (the longest in the beatmap), so it is factored out here - baseBeatLength = timingPoints[i].BeatLength / Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier; + baseBeatLength = timingPoints[i].BeatLength / Beatmap.Difficulty.SliderMultiplier; } } } // Merge sequences of timing and difficulty control points to create the aggregate "multiplier" control point var lastTimingPoint = new TimingControlPoint(); - var lastDifficultyPoint = new DifficultyControlPoint(); + var lastEffectPoint = new EffectControlPoint(); var allPoints = new SortedList(Comparer.Default); + allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints); - allPoints.AddRange(Beatmap.ControlPointInfo.DifficultyPoints); + allPoints.AddRange(Beatmap.ControlPointInfo.EffectPoints); // Generate the timing points, making non-timing changes use the previous timing change and vice-versa var timingChanges = allPoints.Select(c => { - if (c is TimingControlPoint timingPoint) - lastTimingPoint = timingPoint; - else if (c is DifficultyControlPoint difficultyPoint) - lastDifficultyPoint = difficultyPoint; + switch (c) + { + case TimingControlPoint timingPoint: + lastTimingPoint = timingPoint; + break; + + case EffectControlPoint difficultyPoint: + lastEffectPoint = difficultyPoint; + break; + } return new MultiplierControlPoint(c.Time) { - Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier, + Velocity = Beatmap.Difficulty.SliderMultiplier, BaseBeatLength = baseBeatLength, TimingPoint = lastTimingPoint, - DifficultyPoint = lastDifficultyPoint + EffectPoint = lastEffectPoint }; }); @@ -172,7 +179,7 @@ private void load() ControlPoints.AddRange(timingChanges); if (ControlPoints.Count == 0) - ControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier }); + ControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.Difficulty.SliderMultiplier }); } protected override void LoadComplete() diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index c8c53388afae..bf4f3ef4f417 100755 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -25,7 +25,7 @@ namespace osu.Game.Scoring { - public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles, IPostImports + public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles { private readonly Scheduler scheduler; private readonly Func difficulties; @@ -72,9 +72,12 @@ public async Task OrderByTotalScoreAsync(ScoreInfo[] scores, Cancel } } - // We're calling .Result, but this should not be a blocking call due to the above GetDifficultyAsync() calls. - return scores.OrderByDescending(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken).Result) - .ThenBy(s => s.OnlineScoreID) + var totalScores = await Task.WhenAll(scores.Select(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken))).ConfigureAwait(false); + + return scores.Select((score, index) => (score, totalScore: totalScores[index])) + .OrderByDescending(g => g.totalScore) + .ThenBy(g => g.score.OnlineScoreID) + .Select(g => g.score) .ToArray(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 730f482f83fd..6b32ff96c4ef 100755 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -5,14 +5,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { public abstract class CircularDistanceSnapGrid : DistanceSnapGrid { - protected CircularDistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null) - : base(startPosition, startTime, endTime) + protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) + : base(referenceObject, startPosition, startTime, endTime) { } @@ -79,7 +80,7 @@ public override (Vector2 position, double time) GetSnappedPosition(Vector2 posit Vector2 normalisedDirection = direction * new Vector2(1f / distance); Vector2 snappedPosition = StartPosition + normalisedDirection * radialCount * radius; - return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(StartTime, (snappedPosition - StartPosition).Length)); + return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(ReferenceObject, (snappedPosition - StartPosition).Length)); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 59f88ac6417a..9d43e3258afa 100755 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -9,6 +9,7 @@ using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components @@ -54,15 +55,20 @@ public abstract class DistanceSnapGrid : CompositeDrawable private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); private readonly double? endTime; + protected readonly HitObject ReferenceObject; + /// /// Creates a new . /// + /// A reference object to gather relevant difficulty values from. /// The position at which the grid should start. The first tick is located one distance spacing length away from this point. /// The snapping time at . /// The time at which the snapping grid should end. If null, the grid will continue until the bounds of the screen are exceeded. - protected DistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null) + protected DistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) { + ReferenceObject = referenceObject; this.endTime = endTime; + StartPosition = startPosition; StartTime = startTime; @@ -80,7 +86,7 @@ protected override void LoadComplete() private void updateSpacing() { - DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime); + DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject); if (endTime == null) MaxIntervals = int.MaxValue; @@ -88,7 +94,7 @@ private void updateSpacing() { // +1 is added since a snapped hitobject may have its start time slightly less than the snapped time due to floating point errors double maxDuration = endTime.Value - StartTime + 1; - MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(StartTime, DistanceSpacing)); + MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(ReferenceObject, DistanceSpacing)); } gridCache.Invalidate(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index 324893676507..21457ea273ca 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -1,27 +1,106 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit.Timing; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { - public class DifficultyPointPiece : TopPointPiece + public class DifficultyPointPiece : HitObjectPointPiece, IHasPopover { + private readonly HitObject hitObject; + private readonly BindableNumber speedMultiplier; - public DifficultyPointPiece(DifficultyControlPoint point) - : base(point) + public DifficultyPointPiece(HitObject hitObject) + : base(hitObject.DifficultyControlPoint) { - speedMultiplier = point.SpeedMultiplierBindable.GetBoundCopy(); + this.hitObject = hitObject; - Y = Height; + speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy(); } protected override void LoadComplete() { base.LoadComplete(); + speedMultiplier.BindValueChanged(multiplier => Label.Text = $"{multiplier.NewValue:n2}x", true); } + + protected override bool OnClick(ClickEvent e) + { + this.ShowPopover(); + return true; + } + + public Popover GetPopover() => new DifficultyEditPopover(hitObject); + + public class DifficultyEditPopover : OsuPopover + { + private readonly HitObject hitObject; + private readonly DifficultyControlPoint point; + + private SliderWithTextBoxInput sliderVelocitySlider; + + [Resolved(canBeNull: true)] + private EditorBeatmap beatmap { get; set; } + + public DifficultyEditPopover(HitObject hitObject) + { + this.hitObject = hitObject; + point = hitObject.DifficultyControlPoint; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new FillFlowContainer + { + Width = 200, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + sliderVelocitySlider = new SliderWithTextBoxInput("Velocity") + { + Current = new DifficultyControlPoint().SliderVelocityBindable, + KeyboardStep = 0.1f + }, + new OsuTextFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Text = "Hold shift while dragging the end of an object to adjust velocity while snapping." + } + } + } + }; + + var selectedPointBindable = point.SliderVelocityBindable; + + // 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().SliderVelocityBindable.Precision; + if (selectedPointBindable.Precision < expectedPrecision) + selectedPointBindable.Precision = expectedPrecision; + + sliderVelocitySlider.Current = selectedPointBindable; + sliderVelocitySlider.Current.BindValueChanged(_ => beatmap?.Update(hitObject)); + } + } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs new file mode 100644 index 000000000000..6b62459c9757 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public class HitObjectPointPiece : CircularContainer + { + private readonly ControlPoint point; + + protected OsuSpriteText Label { get; private set; } + + protected HitObjectPointPiece(ControlPoint point) + { + this.point = point; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AutoSizeAxes = Axes.Both; + + Color4 colour = point.GetRepresentingColour(colours); + + InternalChildren = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.X, + Height = 16, + Masking = true, + CornerRadius = 8, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Children = new Drawable[] + { + new Box + { + Colour = colour, + RelativeSizeAxes = Axes.Both, + }, + Label = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(5), + Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold), + Colour = colours.B5, + } + } + }, + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 9461f5e885f1..6a26f69e412c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -3,88 +3,102 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osuTK.Graphics; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit.Timing; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { - public class SamplePointPiece : CompositeDrawable + public class SamplePointPiece : HitObjectPointPiece, IHasPopover { - private readonly SampleControlPoint samplePoint; + private readonly HitObject hitObject; private readonly Bindable bank; private readonly BindableNumber volume; - private OsuSpriteText text; - private Container volumeBox; - - private const int max_volume_height = 22; - - public SamplePointPiece(SampleControlPoint samplePoint) + public SamplePointPiece(HitObject hitObject) + : base(hitObject.SampleControlPoint) { - this.samplePoint = samplePoint; - volume = samplePoint.SampleVolumeBindable.GetBoundCopy(); - bank = samplePoint.SampleBankBindable.GetBoundCopy(); + this.hitObject = hitObject; + volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy(); + bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy(); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - Margin = new MarginPadding { Vertical = 5 }; + volume.BindValueChanged(volume => updateText()); + bank.BindValueChanged(bank => updateText(), true); + } + + protected override bool OnClick(ClickEvent e) + { + this.ShowPopover(); + return true; + } + + private void updateText() + { + Label.Text = $"{bank.Value} {volume.Value}"; + } + + public Popover GetPopover() => new SampleEditPopover(hitObject); - Origin = Anchor.BottomCentre; - Anchor = Anchor.BottomCentre; + public class SampleEditPopover : OsuPopover + { + private readonly HitObject hitObject; + private readonly SampleControlPoint point; - AutoSizeAxes = Axes.X; - RelativeSizeAxes = Axes.Y; + private LabelledTextBox bank; + private SliderWithTextBoxInput volume; - Color4 colour = samplePoint.GetRepresentingColour(colours); + [Resolved(canBeNull: true)] + private EditorBeatmap beatmap { get; set; } - InternalChildren = new Drawable[] + public SampleEditPopover(HitObject hitObject) { - volumeBox = new Circle - { - CornerRadius = 5, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Y = -20, - Width = 10, - Colour = colour, - }, - new Container + this.hitObject = hitObject; + point = hitObject.SampleControlPoint; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] { - AutoSizeAxes = Axes.X, - Height = 16, - Masking = true, - CornerRadius = 8, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Children = new Drawable[] + new FillFlowContainer { - new Box + Width = 200, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Colour = colour, - RelativeSizeAxes = Axes.Both, - }, - text = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding(5), - Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold), - Colour = colours.B5, + bank = new LabelledTextBox + { + Label = "Bank Name", + }, + volume = new SliderWithTextBoxInput("Volume") + { + Current = new SampleControlPoint().SampleVolumeBindable, + } } } - }, - }; + }; + + bank.Current = point.SampleBankBindable; + bank.Current.BindValueChanged(_ => beatmap.Update(hitObject)); - volume.BindValueChanged(volume => volumeBox.Height = max_volume_height * volume.NewValue / 100f, true); - bank.BindValueChanged(bank => text.Text = bank.NewValue, true); + volume.Current = point.SampleVolumeBindable; + volume.Current.BindValueChanged(_ => beatmap.Update(hitObject)); + } } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 712afc4de77c..3d4911b20e6f 100755 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -15,6 +15,7 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -58,7 +59,7 @@ public class Timeline : ZoomableScrollContainer, IPositionSnapProvider private Track track; private const float timeline_height = 72; - private const float timeline_expanded_height = 156; + private const float timeline_expanded_height = 94; public Timeline(Drawable userContent) { @@ -158,7 +159,7 @@ protected override void LoadComplete() if (visible.NewValue) { this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint); - mainContent.MoveToY(36, 200, Easing.OutQuint); + mainContent.MoveToY(20, 200, Easing.OutQuint); // delay the fade in else masking looks weird. controlPoints.Delay(180).FadeIn(400, Easing.OutQuint); @@ -298,14 +299,14 @@ public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition private double getTimeFromPosition(Vector2 localPosition) => (localPosition.X / Content.DrawWidth) * track.Length; - public float GetBeatSnapDistanceAt(double referenceTime) => throw new NotImplementedException(); + public float GetBeatSnapDistanceAt(HitObject referenceObject) => throw new NotImplementedException(); - public float DurationToDistance(double referenceTime, double duration) => throw new NotImplementedException(); + public float DurationToDistance(HitObject referenceObject, double duration) => throw new NotImplementedException(); - public double DistanceToDuration(double referenceTime, float distance) => throw new NotImplementedException(); + public double DistanceToDuration(HitObject referenceObject, float distance) => throw new NotImplementedException(); - public double GetSnappedDurationFromDistance(double referenceTime, float distance) => throw new NotImplementedException(); + public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => throw new NotImplementedException(); - public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => throw new NotImplementedException(); + public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index c4beb40f921e..2b2e66fb1849 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -45,17 +45,9 @@ protected override void LoadComplete() { switch (point) { - case DifficultyControlPoint difficultyPoint: - AddInternal(new DifficultyPointPiece(difficultyPoint) { Depth = -2 }); - break; - case TimingControlPoint timingPoint: AddInternal(new TimingPointPiece(timingPoint)); break; - - case SampleControlPoint samplePoint: - AddInternal(new SamplePointPiece(samplePoint) { Depth = -1 }); - break; } } }, true); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 911c9fea5133..e2458d45c9fb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -13,7 +13,9 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Edit; @@ -179,6 +181,15 @@ private void updateColour() colouredComponents.Colour = OsuColour.ForegroundTextColourFor(col); } + private SamplePointPiece sampleOverrideDisplay; + private DifficultyPointPiece difficultyOverrideDisplay; + + [Resolved] + private EditorBeatmap beatmap { get; set; } + + private DifficultyControlPoint difficultyControlPoint; + private SampleControlPoint sampleControlPoint; + protected override void Update() { base.Update(); @@ -194,6 +205,36 @@ protected override void Update() if (Item is IHasRepeats repeats) updateRepeats(repeats); } + + if (difficultyControlPoint != Item.DifficultyControlPoint) + { + difficultyControlPoint = Item.DifficultyControlPoint; + difficultyOverrideDisplay?.Expire(); + + if (Item.DifficultyControlPoint != null && Item is IHasDistance) + { + AddInternal(difficultyOverrideDisplay = new DifficultyPointPiece(Item) + { + Anchor = Anchor.TopLeft, + Origin = Anchor.BottomCentre + }); + } + } + + if (sampleControlPoint != Item.SampleControlPoint) + { + sampleControlPoint = Item.SampleControlPoint; + sampleOverrideDisplay?.Expire(); + + if (Item.SampleControlPoint != null) + { + AddInternal(sampleOverrideDisplay = new SamplePointPiece(Item) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopCentre + }); + } + } } private void updateRepeats(IHasRepeats repeats) @@ -331,39 +372,66 @@ protected override bool OnDragStart(DragStartEvent e) return true; } + private ScheduledDelegate dragOperation; + protected override void OnDrag(DragEvent e) { base.OnDrag(e); - OnDragHandled?.Invoke(e); - - if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time) + // schedule is temporary to ensure we don't process multiple times on a single update frame. we need to find a better method of doing this. + // without it, a hitobject's endtime may not always be in a valid state (ie. sliders, which needs to recompute their path). + dragOperation?.Cancel(); + dragOperation = Scheduler.Add(() => { - switch (hitObject) + OnDragHandled?.Invoke(e); + + if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time) { - case IHasRepeats repeatHitObject: - // find the number of repeats which can fit in the requested time. - var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); - var proposedCount = Math.Max(0, (int)Math.Round((time - hitObject.StartTime) / lengthOfOneRepeat) - 1); + switch (hitObject) + { + case IHasRepeats repeatHitObject: + double proposedDuration = time - hitObject.StartTime; - if (proposedCount == repeatHitObject.RepeatCount) - return; + if (e.CurrentState.Keyboard.ShiftPressed) + { + if (hitObject.DifficultyControlPoint == DifficultyControlPoint.DEFAULT) + hitObject.DifficultyControlPoint = new DifficultyControlPoint(); - repeatHitObject.RepeatCount = proposedCount; - beatmap.Update(hitObject); - break; + var newVelocity = hitObject.DifficultyControlPoint.SliderVelocity * (repeatHitObject.Duration / proposedDuration); - case IHasDuration endTimeHitObject: - var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); + if (Precision.AlmostEquals(newVelocity, hitObject.DifficultyControlPoint.SliderVelocity)) + return; - if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, beatmap.GetBeatLengthAtTime(snappedTime))) - return; + hitObject.DifficultyControlPoint.SliderVelocity = newVelocity; + beatmap.Update(hitObject); + } + else + { + // find the number of repeats which can fit in the requested time. + var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); + var proposedCount = Math.Max(0, (int)Math.Round(proposedDuration / lengthOfOneRepeat) - 1); - endTimeHitObject.Duration = snappedTime - hitObject.StartTime; - beatmap.Update(hitObject); - break; + if (proposedCount == repeatHitObject.RepeatCount) + return; + + repeatHitObject.RepeatCount = proposedCount; + beatmap.Update(hitObject); + } + + break; + + case IHasDuration endTimeHitObject: + var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); + + if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, beatmap.GetBeatLengthAtTime(snappedTime))) + return; + + endTimeHitObject.Duration = snappedTime - hitObject.StartTime; + beatmap.Update(hitObject); + break; + } } - } + }); } protected override void OnDragEnd(DragEndEvent e) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b213f5b5f6c0..e073144b863f 100755 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -162,7 +162,7 @@ private void load(OsuColour colours, OsuConfigManager config) // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); - AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin())); + AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin(), loadableBeatmap.BeatmapInfo)); dependencies.CacheAs(editorBeatmap); changeHandler = new EditorChangeHandler(editorBeatmap); dependencies.CacheAs(changeHandler); @@ -333,10 +333,10 @@ protected void Save() isNewBeatmap = false; // apply any set-level metadata changes. - beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); + beatmapManager.Update(editorBeatmap.BeatmapInfo.BeatmapSet); // save the loaded beatmap's data stream. - beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin); + beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin); updateLastSavedHash(); } @@ -508,6 +508,7 @@ public override bool OnExiting(IScreen next) if (isNewBeatmap || HasUnsavedChanges) { + samplePlaybackDisabled.Value = true; dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave, cancelExit)); return true; } @@ -522,7 +523,10 @@ public override bool OnExiting(IScreen next) var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo); if (!(refetchedBeatmap is DummyWorkingBeatmap)) + { + Logger.Log("Editor providing re-fetched beatmap post edit session"); Beatmap.Value = refetchedBeatmap; + } return base.OnExiting(next); } @@ -756,7 +760,11 @@ private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo) ClipboardContent = editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? clipboard.Value : string.Empty }); - private void cancelExit() => loader?.CancelPendingDifficultySwitch(); + private void cancelExit() + { + samplePlaybackDisabled.Value = false; + loader?.CancelPendingDifficultySwitch(); + } public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 3402bf653aaf..2e84ef437a44 100755 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -44,6 +45,7 @@ public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapPr /// public readonly Bindable PlacementObject = new Bindable(); + private readonly BeatmapInfo beatmapInfo; public readonly IBeatmap PlayableBeatmap; /// @@ -66,9 +68,37 @@ public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapPr private readonly Dictionary> startTimeBindables = new Dictionary>(); - public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null) + public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null, BeatmapInfo beatmapInfo = null) { PlayableBeatmap = playableBeatmap; + + // ensure we are not working with legacy control points. + // if we leave the legacy points around they will be applied over any local changes on + // ApplyDefaults calls. this should eventually be removed once the default logic is moved to the decoder/converter. + if (PlayableBeatmap.ControlPointInfo is LegacyControlPointInfo) + { + var newControlPoints = new ControlPointInfo(); + + foreach (var controlPoint in PlayableBeatmap.ControlPointInfo.AllControlPoints) + { + switch (controlPoint) + { + case DifficultyControlPoint _: + case SampleControlPoint _: + // skip legacy types. + continue; + + default: + newControlPoints.Add(controlPoint.Time, controlPoint); + break; + } + } + + playableBeatmap.ControlPointInfo = newControlPoints; + } + + this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo; + if (beatmapSkin is Skin skin) BeatmapSkin = new EditorBeatmapSkin(skin); @@ -80,11 +110,17 @@ public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null) public BeatmapInfo BeatmapInfo { - get => PlayableBeatmap.BeatmapInfo; - set => PlayableBeatmap.BeatmapInfo = value; + get => beatmapInfo; + set => throw new InvalidOperationException(); } - public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; + public BeatmapMetadata Metadata => beatmapInfo.Metadata; + + public BeatmapDifficulty Difficulty + { + get => PlayableBeatmap.Difficulty; + set => PlayableBeatmap.Difficulty = value; + } public ControlPointInfo ControlPointInfo { @@ -286,7 +322,7 @@ protected override void UpdateState() /// public void Clear() => RemoveRange(HitObjects.ToArray()); - private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty); + private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, PlayableBeatmap.Difficulty); private void trackStartTime(HitObject hitObject) { diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 25806abcc7ae..2352fc1390ce 100755 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -25,7 +25,9 @@ public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourc public double TrackLength => track.Value?.Length ?? 60000; - public ControlPointInfo ControlPointInfo; + public ControlPointInfo ControlPointInfo => Beatmap.ControlPointInfo; + + public IBeatmap Beatmap { get; set; } private readonly BindableBeatDivisor beatDivisor; @@ -42,25 +44,15 @@ public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourc /// public bool IsSeeking { get; private set; } - public EditorClock(IBeatmap beatmap, BindableBeatDivisor beatDivisor) - : this(beatmap.ControlPointInfo, beatDivisor) - { - } - - public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor) + public EditorClock(IBeatmap beatmap = null, BindableBeatDivisor beatDivisor = null) { - this.beatDivisor = beatDivisor; + Beatmap = beatmap ?? new Beatmap(); - ControlPointInfo = controlPointInfo; + this.beatDivisor = beatDivisor ?? new BindableBeatDivisor(); underlyingClock = new DecoupleableInterpolatingFramedClock(); } - public EditorClock() - : this(new ControlPointInfo(), new BindableBeatDivisor()) - { - } - /// /// Seek to the closest snappable beat from a time. /// diff --git a/osu.Game/Screens/Edit/EditorRoundedScreen.cs b/osu.Game/Screens/Edit/EditorRoundedScreen.cs index b271a145f528..508663224d74 100644 --- a/osu.Game/Screens/Edit/EditorRoundedScreen.cs +++ b/osu.Game/Screens/Edit/EditorRoundedScreen.cs @@ -41,7 +41,7 @@ private void load() { new Box { - Colour = ColourProvider.Dark4, + Colour = ColourProvider.Background3, RelativeSizeAxes = Axes.Both, }, roundedContent = new Container diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index b9483c175004..9025375198f5 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -30,7 +30,7 @@ private void load() Label = "物件大小(CS)", FixedLabelWidth = LABEL_WIDTH, Description = "这将决定所有物件的大小, 数值越大, 物件越小", - Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.CircleSize) + Current = new BindableFloat(Beatmap.Difficulty.CircleSize) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -43,7 +43,7 @@ private void load() Label = "掉血速度(HP)", FixedLabelWidth = LABEL_WIDTH, Description = "这将决定掉血的速度, 数值越大, 掉血越快", - Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.DrainRate) + Current = new BindableFloat(Beatmap.Difficulty.DrainRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -56,7 +56,7 @@ private void load() Label = "缩圈速度(AR)", FixedLabelWidth = LABEL_WIDTH, Description = "这将决定缩圈速度, 数值越大, 缩圈越快", - Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate) + Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -69,7 +69,7 @@ private void load() Label = "整体难度(OD)", FixedLabelWidth = LABEL_WIDTH, Description = "这将决定该谱面的整体难度(准确率要求, 转盘难度等), 数值越大, 难度越高", - Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) + Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -87,10 +87,10 @@ private void onValueChanged(ValueChangedEvent args) { // for now, update these on commit rather than making BeatmapMetadata bindables. // after switching database engines we can reconsider if switching to bindables is a good direction. - Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value; - Beatmap.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value; - Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value; - Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value; + Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value; + Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; + Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value; + Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.UpdateAllHitObjects(); } diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 9e88fa92a338..b5319439c940 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -10,7 +10,7 @@ namespace osu.Game.Screens.Edit.Setup { - internal class MetadataSection : SetupSection + public class MetadataSection : SetupSection { protected LabelledTextBox ArtistTextBox; protected LabelledTextBox RomanisedArtistTextBox; diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs index 48639789af2a..938c7f9cf0b3 100755 --- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs @@ -12,8 +12,6 @@ public class ControlPointSettings : EditorRoundedScreenSettings { new GroupSection(), new TimingSection(), - new DifficultySection(), - new SampleSection(), new EffectSection(), }; } diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs deleted file mode 100755 index 33b169ad5823..000000000000 --- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Game.Beatmaps.ControlPoints; - -namespace osu.Game.Screens.Edit.Timing -{ - internal class DifficultySection : Section - { - private SliderWithTextBoxInput multiplierSlider; - - [BackgroundDependencyLoader] - private void load() - { - Flow.AddRange(new[] - { - multiplierSlider = new SliderWithTextBoxInput("速度倍率") - { - Current = new DifficultyControlPoint().SpeedMultiplierBindable, - KeyboardStep = 0.1f - } - }); - } - - protected override void OnControlPointChanged(ValueChangedEvent point) - { - if (point.NewValue != null) - { - 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()); - } - } - - protected override DifficultyControlPoint CreatePoint() - { - var reference = Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time); - - return new DifficultyControlPoint - { - SpeedMultiplier = reference.SpeedMultiplier, - }; - } - } -} diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index 2c83134fe66c..b7837e905f05 100755 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterfaceV2; @@ -13,13 +14,20 @@ internal class EffectSection : Section private LabelledSwitchButton kiai; private LabelledSwitchButton omitBarLine; + private SliderWithTextBoxInput scrollSpeedSlider; + [BackgroundDependencyLoader] private void load() { - Flow.AddRange(new[] + Flow.AddRange(new Drawable[] { - kiai = new LabelledSwitchButton { Label = "高潮/副歌" }, - omitBarLine = new LabelledSwitchButton { Label = "移除第一条横线" }, + kiai = new LabelledSwitchButton { Label = "高潮、副歌" }, + omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" }, + scrollSpeedSlider = new SliderWithTextBoxInput("滚动速度") + { + Current = new EffectControlPoint().ScrollSpeedBindable, + KeyboardStep = 0.1f + } }); } @@ -32,6 +40,9 @@ protected override void OnControlPointChanged(ValueChangedEvent ChangeHandler?.SaveState()); + + scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable; + scrollSpeedSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } @@ -42,7 +53,8 @@ protected override EffectControlPoint CreatePoint() return new EffectControlPoint { KiaiMode = reference.KiaiMode, - OmitFirstBarLine = reference.OmitFirstBarLine + OmitFirstBarLine = reference.OmitFirstBarLine, + ScrollSpeed = reference.ScrollSpeed, }; } } diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs index 7b553ac7ad09..a8de476d674a 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs @@ -18,7 +18,7 @@ public class DifficultyRowAttribute : RowAttribute public DifficultyRowAttribute(DifficultyControlPoint difficulty) : base(difficulty, "difficulty") { - speedMultiplier = difficulty.SpeedMultiplierBindable.GetBoundCopy(); + speedMultiplier = difficulty.SliderVelocityBindable.GetBoundCopy(); } [BackgroundDependencyLoader] @@ -32,7 +32,7 @@ private void load() }, text = new AttributeText(Point) { - Width = 40, + Width = 45, }, }); diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs index 812407d6daed..1b33fd62aa18 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs @@ -12,14 +12,18 @@ public class EffectRowAttribute : RowAttribute { private readonly Bindable kiaiMode; private readonly Bindable omitBarLine; + private readonly BindableNumber scrollSpeed; + private AttributeText kiaiModeBubble; private AttributeText omitBarLineBubble; + private AttributeText text; public EffectRowAttribute(EffectControlPoint effect) : base(effect, "effect") { kiaiMode = effect.KiaiModeBindable.GetBoundCopy(); omitBarLine = effect.OmitFirstBarLineBindable.GetBoundCopy(); + scrollSpeed = effect.ScrollSpeedBindable.GetBoundCopy(); } [BackgroundDependencyLoader] @@ -27,12 +31,20 @@ private void load() { Content.AddRange(new Drawable[] { + new AttributeProgressBar(Point) + { + Current = scrollSpeed, + }, + text = new AttributeText(Point) { Width = 45 }, kiaiModeBubble = new AttributeText(Point) { Text = "kiai" }, omitBarLineBubble = new AttributeText(Point) { Text = "no barline" }, }); kiaiMode.BindValueChanged(enabled => kiaiModeBubble.FadeTo(enabled.NewValue ? 1 : 0), true); omitBarLine.BindValueChanged(enabled => omitBarLineBubble.FadeTo(enabled.NewValue ? 1 : 0), true); + scrollSpeed.BindValueChanged(_ => updateText(), true); } + + private void updateText() => text.Text = $"{scrollSpeed.Value:n2}x"; } } diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs deleted file mode 100755 index bcc07107a100..000000000000 --- a/osu.Game/Screens/Edit/Timing/SampleSection.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.UserInterfaceV2; - -namespace osu.Game.Screens.Edit.Timing -{ - internal class SampleSection : Section - { - private LabelledTextBox bank; - private SliderWithTextBoxInput volume; - - [BackgroundDependencyLoader] - private void load() - { - Flow.AddRange(new Drawable[] - { - bank = new LabelledTextBox - { - Label = "Bank name", - }, - volume = new SliderWithTextBoxInput("音量") - { - Current = new SampleControlPoint().SampleVolumeBindable, - } - }); - } - - protected override void OnControlPointChanged(ValueChangedEvent point) - { - if (point.NewValue != null) - { - bank.Current = point.NewValue.SampleBankBindable; - bank.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); - - volume.Current = point.NewValue.SampleVolumeBindable; - volume.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); - } - } - - protected override SampleControlPoint CreatePoint() => new SampleControlPoint(); // TODO: remove - } -} diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 90d80ddb1ba5..6ba9cdfadf67 100755 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -47,23 +47,25 @@ private IntroScreen getIntroSequence() switch (introSequence) { case IntroSequence.Circles: - return new IntroCircles(false); + return new IntroCircles(createMainMenu); case IntroSequence.CirclesCN: - return new IntroCircles(true); + return new IntroCircles(createMainMenu, true); case IntroSequence.TrianglesCN: - return new IntroTriangles(true); + return new IntroTriangles(createMainMenu, true); case IntroSequence.SkippedIntro: - return new IntroSkipped(); + return new IntroSkipped(createMainMenu); case IntroSequence.Welcome: - return new IntroWelcome(); + return new IntroWelcome(createMainMenu); default: - return new IntroTriangles(); + return new IntroTriangles(createMainMenu); } + + MainMenu createMainMenu() => new MainMenu(); } protected virtual ShaderPrecompiler CreateShaderPrecompiler() => new ShaderPrecompiler(); diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs index 15bd0c5a8fba..78f9929c77cd 100644 --- a/osu.Game/Screens/Menu/IntroCircles.cs +++ b/osu.Game/Screens/Menu/IntroCircles.cs @@ -1,6 +1,8 @@ // 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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -24,7 +26,8 @@ public class IntroCircles : IntroScreen private Sample welcome; private readonly bool useTranslate; - public IntroCircles(bool useTranslate = false) + public IntroCircles([CanBeNull] Func createNextScreen = null, bool useTranslate = false) + : base(createNextScreen) { this.useTranslate = useTranslate; } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index e1835fe004a1..f5dad2e54f6c 100755 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -1,16 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Utils; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.IO.Archives; @@ -56,7 +58,7 @@ public abstract class IntroScreen : StartupScreen private LeasedBindable beatmap; - private MainMenu mainMenu; + private OsuScreen nextScreen; protected BeatmapSetInfo SetInfo; @@ -74,12 +76,20 @@ public abstract class IntroScreen : StartupScreen private BeatmapManager beatmaps { get; set; } + [CanBeNull] + private readonly Func createNextScreen; + /// /// Whether the is provided by osu! resources, rather than a user beatmap. /// Only valid during or after . /// protected bool UsingThemedIntro { get; private set; } + protected IntroScreen([CanBeNull] Func createNextScreen = null) + { + this.createNextScreen = createNextScreen; + } + [BackgroundDependencyLoader] private void load(OsuConfigManager config, SkinManager skinManager, BeatmapManager beatmaps, Framework.Game game) { @@ -242,14 +252,21 @@ protected override void LogoArriving(OsuLogo logo, bool resuming) } } - protected void PrepareMenuLoad() => LoadComponentAsync(mainMenu = new MainMenu()); + protected void PrepareMenuLoad() + { + nextScreen = createNextScreen?.Invoke(); + + if (nextScreen != null) + LoadComponentAsync(nextScreen); + } protected virtual void LoadMenu() { beatmap.Return(); DidLoadMenu = true; - this.Push(mainMenu); + if (nextScreen != null) + this.Push(nextScreen); } } } diff --git a/osu.Game/Screens/Menu/IntroSkipped.cs b/osu.Game/Screens/Menu/IntroSkipped.cs index bbdb287dfed0..01963219b407 100644 --- a/osu.Game/Screens/Menu/IntroSkipped.cs +++ b/osu.Game/Screens/Menu/IntroSkipped.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.Allocation; @@ -26,6 +27,11 @@ public class IntroSkipped : IntroScreen [Resolved(CanBeNull = true)] private OsuGame game { get; set; } + public IntroSkipped(Func createScreen) + : base(createScreen) + { + } + [BackgroundDependencyLoader] private void load(MConfigManager config) { diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index d824536db36d..bc8cf160ce5d 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -42,14 +43,15 @@ public class IntroTriangles : IntroScreen private Sample welcome; private readonly bool useTranslate; - public IntroTriangles(bool useTranslate = false) + private DecoupleableInterpolatingFramedClock decoupledClock; + private TrianglesIntroSequence intro; + + public IntroTriangles([CanBeNull] Func createNextScreen = null, bool useTranslate = false) + : base(createNextScreen) { this.useTranslate = useTranslate; } - private DecoupleableInterpolatingFramedClock decoupledClock; - private TrianglesIntroSequence intro; - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index f74043b0456d..639591cfef4d 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -1,6 +1,8 @@ // 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 JetBrains.Annotations; using osuTK; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -32,6 +34,11 @@ public class IntroWelcome : IntroScreen private BackgroundScreenDefault background; + public IntroWelcome([CanBeNull] Func createNextScreen = null) + : base(createNextScreen) + { + } + [BackgroundDependencyLoader] private void load(AudioManager audio) { diff --git a/osu.Game/Screens/Mvis/MvisScreen.cs b/osu.Game/Screens/Mvis/MvisScreen.cs index fd22d8b8f386..9662179b6a64 100644 --- a/osu.Game/Screens/Mvis/MvisScreen.cs +++ b/osu.Game/Screens/Mvis/MvisScreen.cs @@ -914,9 +914,10 @@ public override void OnResuming(IScreen last) public bool OnPressed(KeyBindingPressEvent action) { //查找本体按键绑定 - keyBindings.FirstOrDefault(b => b.Key == action.Action).Value?.Invoke(); + var target = keyBindings.FirstOrDefault(b => b.Key == action.Action).Value; + target?.Invoke(); - return false; + return target != null; } public void OnReleased(KeyBindingReleaseEvent action) { } @@ -932,9 +933,10 @@ protected override bool Handle(UIEvent e) protected override bool OnKeyDown(KeyDownEvent e) { //查找插件按键绑定并执行 - pluginKeyBindings.FirstOrDefault(b => b.Key.Key == e.Key).Key?.Action?.Invoke(); + var target = pluginKeyBindings.FirstOrDefault(b => b.Key.Key == e.Key).Key; + target?.Action?.Invoke(); - return base.OnKeyDown(e); + return target != null; } //当有弹窗或游戏失去焦点时要进行的动作 diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 381849189d11..abda9e897b03 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -116,8 +116,6 @@ public void AddOrUpdateRoom(Room room) if (ignoredRooms.Contains(room.RoomID.Value.Value)) return; - room.Position.Value = -room.RoomID.Value.Value; - try { foreach (var pi in room.Playlist) @@ -152,6 +150,11 @@ public void ClearRooms() notifyRoomsUpdated(); } - private void notifyRoomsUpdated() => Scheduler.AddOnce(() => RoomsUpdated?.Invoke()); + private void notifyRoomsUpdated() + { + Scheduler.AddOnce(invokeRoomsUpdated); + + void invokeRoomsUpdated() => RoomsUpdated?.Invoke(); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 907b7e308acb..85efdcef1a89 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -129,7 +129,7 @@ private void removeRooms(IEnumerable rooms) private void updateSorting() { foreach (var room in roomFlow) - roomFlow.SetLayoutPosition(room, room.Room.Position.Value); + roomFlow.SetLayoutPosition(room, -(room.Room.RoomID.Value ?? 0)); } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 80a5daa7c8b3..0edf5dde6de0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -99,14 +98,14 @@ public MatchSettings(Room room) } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { InternalChildren = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d"), + Colour = colourProvider.Background4 }, new GridContainer { @@ -249,7 +248,7 @@ private void load(OsuColour colours) new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + Colour = colourProvider.Background5 }, new FillFlowContainer { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 6c3dfe738227..cf1066df10e0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -79,11 +79,13 @@ private class MultiplayerListingPollingComponent : ListingPollingComponent private void load() { isConnected.BindTo(client.IsConnected); - isConnected.BindValueChanged(c => Scheduler.AddOnce(() => - { - if (isConnected.Value && IsLoaded) - PollImmediately(); - }), true); + isConnected.BindValueChanged(c => Scheduler.AddOnce(poll), true); + } + + private void poll() + { + if (isConnected.Value && IsLoaded) + PollImmediately(); } protected override Task Poll() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs index 0f256160eb0a..a380ddef25d7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs @@ -19,15 +19,19 @@ protected override void LoadComplete() { base.LoadComplete(); - Client.RoomUpdated += OnRoomUpdated; - - Client.UserLeft += UserLeft; - Client.UserKicked += UserKicked; - Client.UserJoined += UserJoined; + Client.RoomUpdated += invokeOnRoomUpdated; + Client.UserLeft += invokeUserLeft; + Client.UserKicked += invokeUserKicked; + Client.UserJoined += invokeUserJoined; OnRoomUpdated(); } + private void invokeOnRoomUpdated() => Scheduler.AddOnce(OnRoomUpdated); + private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.AddOnce(UserJoined, user); + private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(UserKicked, user); + private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.AddOnce(UserLeft, user); + /// /// Invoked when a user has joined the room. /// @@ -63,10 +67,10 @@ protected override void Dispose(bool isDisposing) { if (Client != null) { - Client.UserLeft -= UserLeft; - Client.UserKicked -= UserKicked; - Client.UserJoined -= UserJoined; - Client.RoomUpdated -= OnRoomUpdated; + Client.RoomUpdated -= invokeOnRoomUpdated; + Client.UserLeft -= invokeUserLeft; + Client.UserKicked -= invokeUserKicked; + Client.UserJoined -= invokeUserJoined; } base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index fcbbdd56bed5..82e678815aea 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -5,7 +5,6 @@ using System.Collections.Specialized; using Humanizer; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -77,14 +76,14 @@ public MatchSettings(Room room) } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { InternalChildren = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d"), + Colour = colourProvider.Background4 }, new GridContainer { @@ -256,7 +255,7 @@ private void load(OsuColour colours) new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + Colour = colourProvider.Background5 }, new FillFlowContainer { diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 71bea2a1455e..2a1c4599d5d7 100755 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -6,11 +6,15 @@ using osu.Game.Rulesets.UI; using System; using System.Collections.Generic; +using ManagedBass.Fx; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; +using osu.Game.Audio.Effects; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; using osuTK; @@ -20,25 +24,40 @@ namespace osu.Game.Screens.Play { /// /// Manage the animation to be applied when a player fails. - /// Single file; automatically disposed after use. + /// Single use and automatically disposed after use. /// - public class FailAnimation : Component + public class FailAnimation : Container { public Action OnComplete; private readonly DrawableRuleset drawableRuleset; - private readonly BindableDouble trackFreq = new BindableDouble(1); + private Container filters; + + private Box failFlash; + private Track track; + private AudioFilter failLowPassFilter; + private AudioFilter failHighPassFilter; + private const float duration = 2500; private Sample failSample; + protected override Container Content { get; } = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }; + public FailAnimation(DrawableRuleset drawableRuleset) { this.drawableRuleset = drawableRuleset; + + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] @@ -46,6 +65,27 @@ private void load(AudioManager audio, IBindable beatmap) { track = beatmap.Value.Track; failSample = audio.Samples.Get(@"Gameplay/failsound"); + + AddRangeInternal(new Drawable[] + { + filters = new Container + { + Children = new Drawable[] + { + failLowPassFilter = new AudioFilter(audio.TrackMixer), + failHighPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), + }, + }, + Content, + failFlash = new Box + { + Colour = Color4.Red, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Depth = float.MinValue, + Alpha = 0 + }, + }); } private bool started; @@ -60,19 +100,42 @@ public void Start() started = true; - failSample.Play(); - this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ => { OnComplete?.Invoke(); - Expire(); }); + failHighPassFilter.CutoffTo(300); + failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic); + failSample.Play(); + track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); applyToPlayfield(drawableRuleset.Playfield); - drawableRuleset.Playfield.HitObjectContainer.FlashColour(Color4.Red, 500); drawableRuleset.Playfield.HitObjectContainer.FadeOut(duration / 2); + + failFlash.FadeOutFromOne(1000); + + Content.Masking = true; + + Content.Add(new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }); + + Content.ScaleTo(0.85f, duration, Easing.OutQuart); + Content.RotateTo(1, duration, Easing.OutQuart); + Content.FadeColour(Color4.Gray, duration); + } + + public void RemoveFilters() + { + RemoveInternal(filters); + filters.Dispose(); + + track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); } protected override void Update() @@ -121,11 +184,5 @@ private void dropOffScreen(DrawableHitObject obj, double failTime, float randomR obj.MoveTo(originalPosition + new Vector2(0, 400), duration); } } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); - } } } diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index 63de5c8de599..87b19e8433ef 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -11,7 +11,6 @@ namespace osu.Game.Screens.Play.HUD public class DefaultScoreCounter : GameplayScoreCounter, ISkinnableDrawable { public DefaultScoreCounter() - : base(6) { Anchor = Anchor.TopCentre; Origin = Anchor.TopCentre; diff --git a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs index e09630d2c4fd..e05eff5f3ea5 100644 --- a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs @@ -14,8 +14,8 @@ public abstract class GameplayScoreCounter : ScoreCounter { private Bindable scoreDisplayMode; - protected GameplayScoreCounter(int leading = 0, bool useCommaSeparator = false) - : base(leading, useCommaSeparator) + protected GameplayScoreCounter() + : base(6) { } diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs index d04e60a2ab8c..b1c07512ddaf 100644 --- a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs +++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs @@ -4,11 +4,9 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -148,7 +146,7 @@ protected override void UpdateAfterChildren() Score2Text.X = Math.Max(5 + Score2Text.DrawWidth / 2, score2Bar.DrawWidth); } - protected class MatchScoreCounter : ScoreCounter + protected class MatchScoreCounter : CommaSeparatedScoreCounter { private OsuSpriteText displayedSpriteText; @@ -173,8 +171,6 @@ private void updateFont(bool winning) => displayedSpriteText.Font = winning ? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true) : OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true); - - protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N0"); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 10be0246f05e..bf6602c7a04d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -125,15 +124,11 @@ public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisab public DimmableStoryboard DimmableStoryboard { get; private set; } - [Cached] - [Cached(Type = typeof(IBindable>))] - protected new readonly Bindable> Mods = new Bindable>(Array.Empty()); - /// /// Whether failing should be allowed. /// By default, this checks whether all selected mods allow failing. /// - protected virtual bool CheckModsAllowFailure() => Mods.Value.OfType().All(m => m.PerformFail()); + protected virtual bool CheckModsAllowFailure() => GameplayState.Mods.OfType().All(m => m.PerformFail()); public readonly PlayerConfiguration Configuration; @@ -179,12 +174,12 @@ protected virtual void PrepareReplay() [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) { - Mods.Value = base.Mods.Value.Select(m => m.DeepClone()).ToArray(); + var gameplayMods = Mods.Value.Select(m => m.DeepClone()).ToArray(); if (Beatmap.Value is DummyWorkingBeatmap) return; - IBeatmap playableBeatmap = loadPlayableBeatmap(); + IBeatmap playableBeatmap = loadPlayableBeatmap(gameplayMods); if (playableBeatmap == null) return; @@ -199,12 +194,12 @@ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) if (game is OsuGame osuGame) LocalUserPlaying.BindTo(osuGame.LocalUserPlaying); - DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); + DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, gameplayMods); dependencies.CacheAs(DrawableRuleset); ScoreProcessor = ruleset.CreateScoreProcessor(); ScoreProcessor.ApplyBeatmap(playableBeatmap); - ScoreProcessor.Mods.BindTo(Mods); + ScoreProcessor.Mods.Value = gameplayMods; dependencies.CacheAs(ScoreProcessor); @@ -218,16 +213,16 @@ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); + AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); + Score = CreateScore(playableBeatmap); // ensure the score is in a consistent state with the current player. Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; - Score.ScoreInfo.Mods = Mods.Value.ToArray(); + Score.ScoreInfo.Mods = gameplayMods; - dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value, Score)); - - AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score)); var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); @@ -235,17 +230,53 @@ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. GameplayClockContainer.Add(rulesetSkinProvider); - rulesetSkinProvider.AddRange(new[] + rulesetSkinProvider.AddRange(new Drawable[] { - // underlay and gameplay should have access to the skinning sources. - createUnderlayComponents(), - createGameplayComponents(Beatmap.Value, playableBeatmap) + failAnimationLayer = new FailAnimation(DrawableRuleset) + { + OnComplete = onFailComplete, + Children = new[] + { + // underlay and gameplay should have access to the skinning sources. + createUnderlayComponents(), + createGameplayComponents(Beatmap.Value, playableBeatmap) + } + }, + FailOverlay = new FailOverlay + { + OnRetry = Restart, + OnQuit = () => PerformExit(true), + }, + new HotkeyExitOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + fadeOut(true); + PerformExit(false); + }, + }, }); + if (Configuration.AllowRestart) + { + rulesetSkinProvider.Add(new HotkeyRetryOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + fadeOut(true); + Restart(); + }, + }); + } + // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. - rulesetSkinProvider.Add(createOverlayComponents(Beatmap.Value)); + failAnimationLayer.Add(createOverlayComponents(Beatmap.Value)); if (!DrawableRuleset.AllowGameplayOverlays) { @@ -302,13 +333,13 @@ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) // this is required for mods that apply transforms to these processors. ScoreProcessor.OnLoadComplete += _ => { - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in gameplayMods.OfType()) mod.ApplyToScoreProcessor(ScoreProcessor); }; HealthProcessor.OnLoadComplete += _ => { - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in gameplayMods.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); }; @@ -357,7 +388,7 @@ private Drawable createOverlayComponents(WorkingBeatmap working) // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), - HUDOverlay = new HUDOverlay(DrawableRuleset, Mods.Value) + HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods) { HoldToQuit = { @@ -381,11 +412,6 @@ private Drawable createOverlayComponents(WorkingBeatmap working) RequestSkip = () => progressToResults(false), Alpha = 0 }, - FailOverlay = new FailOverlay - { - OnRetry = Restart, - OnQuit = () => PerformExit(true), - }, PauseOverlay = new PauseOverlay { OnResume = Resume, @@ -393,18 +419,7 @@ private Drawable createOverlayComponents(WorkingBeatmap working) OnRetry = Restart, OnQuit = () => PerformExit(true), }, - new HotkeyExitOverlay - { - Action = () => - { - if (!this.IsCurrentScreen()) return; - - fadeOut(true); - PerformExit(false); - }, - }, - failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, - } + }, }; if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays) @@ -416,20 +431,6 @@ private Drawable createOverlayComponents(WorkingBeatmap working) if (GameplayClockContainer is MasterGameplayClockContainer master) HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate; - if (Configuration.AllowRestart) - { - container.Add(new HotkeyRetryOverlay - { - Action = () => - { - if (!this.IsCurrentScreen()) return; - - fadeOut(true); - Restart(); - }, - }); - } - return container; } @@ -468,7 +469,7 @@ private void updatePauseOnFocusLostState() } } - private IBeatmap loadPlayableBeatmap() + private IBeatmap loadPlayableBeatmap(Mod[] gameplayMods) { IBeatmap playable; @@ -482,7 +483,7 @@ private IBeatmap loadPlayableBeatmap() try { - playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value); + playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, gameplayMods); } catch (BeatmapInvalidForRulesetException) { @@ -490,7 +491,7 @@ private IBeatmap loadPlayableBeatmap() rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset; ruleset = rulesetInfo.CreateInstance(); - playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value); + playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, gameplayMods); } if (playable.HitObjects.Count == 0) @@ -547,7 +548,7 @@ protected void PerformExit(bool showDialogFirst) // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). if (ValidForResume && HasFailed) { - failAnimation.FinishTransforms(true); + failAnimationLayer.FinishTransforms(true); return; } @@ -773,7 +774,7 @@ private void progressToResults(bool withDelay) protected FailOverlay FailOverlay { get; private set; } - private FailAnimation failAnimation; + private FailAnimation failAnimationLayer; private bool onFail() { @@ -789,9 +790,9 @@ private bool onFail() if (PauseOverlay.State.Value == Visibility.Visible) PauseOverlay.Hide(); - failAnimation.Start(); + failAnimationLayer.Start(); - if (Mods.Value.OfType().Any(m => m.RestartOnFail)) + if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) Restart(); return true; @@ -921,17 +922,17 @@ public override void OnEntering(IScreen last) storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in GameplayState.Mods.OfType()) mod.ApplyToPlayer(this); - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in GameplayState.Mods.OfType()) mod.ApplyToHUD(HUDOverlay); // Our mods are local copies of the global mods so they need to be re-applied to the track. // This is done through the music controller (for now), because resetting speed adjustments on the beatmap track also removes adjustments provided by DrawableTrack. // Todo: In the future, player will receive in a track and will probably not have to worry about this... musicController.ResetTrackAdjustments(); - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in GameplayState.Mods.OfType()) mod.ApplyToTrack(musicController.CurrentTrack); updateGameplayState(); @@ -954,7 +955,7 @@ protected virtual void StartGameplay() public override void OnSuspending(IScreen next) { - screenSuspension?.Expire(); + screenSuspension?.RemoveAndDisposeImmediately(); fadeOut(); base.OnSuspending(next); @@ -962,7 +963,8 @@ public override void OnSuspending(IScreen next) public override bool OnExiting(IScreen next) { - screenSuspension?.Expire(); + screenSuspension?.RemoveAndDisposeImmediately(); + failAnimationLayer?.RemoveFilters(); // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. if (prepareScoreForDisplayTask == null) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 45fe90573b68..853304b7191a 100755 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -15,6 +15,7 @@ using osu.Framework.Input; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Audio.Effects; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -37,6 +38,8 @@ public class PlayerLoader : ScreenWithBeatmapBackground protected const float BACKGROUND_BLUR = 15; + private const double content_out_duration = 300; + public override bool HideOverlaysOnEnter => hideOverlays; public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -66,6 +69,8 @@ public class PlayerLoader : ScreenWithBeatmapBackground private readonly BindableDouble volumeAdjustment = new BindableDouble(1); + private AudioFilter lowPassFilter; + protected bool BackgroundBrightnessReduction { set @@ -130,41 +135,45 @@ public PlayerLoader(Func createPlayer) } [BackgroundDependencyLoader] - private void load(SessionStatics sessionStatics, MConfigManager config) + private void load(SessionStatics sessionStatics, MConfigManager config, AudioManager audio) { muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce); config.BindWith(MSetting.OptUI, optui); batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce); - InternalChild = (content = new LogoTrackingContainer + InternalChildren = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }).WithChildren(new Drawable[] - { - MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade) + (content = new LogoTrackingContainer { - Alpha = 0.001f, Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, - PlayerSettings = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + }).WithChildren(new Drawable[] { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Margin = new MarginPadding(25), - Children = new PlayerSettingsGroup[] + MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade) { - VisualSettings = new VisualSettings(), - new InputSettings() - } - }, - idleTracker = new IdleTracker(750) - }); + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + PlayerSettings = new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding(25), + Children = new PlayerSettingsGroup[] + { + VisualSettings = new VisualSettings(), + new InputSettings() + } + }, + idleTracker = new IdleTracker(750), + }), + lowPassFilter = new AudioFilter(audio.TrackMixer) + }; optui.BindValueChanged(_ => updateExtraDelay()); @@ -249,14 +258,16 @@ public override void OnSuspending(IScreen next) // stop the track before removing adjustment to avoid a volume spike. Beatmap.Value.Track.Stop(); Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); + lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF); } public override bool OnExiting(IScreen next) { cancelLoad(); + contentOut(); - content.ScaleTo(0.7f, 150, Easing.InQuint); - this.FadeOut(150); + // Ensure the screen doesn't expire until all the outwards fade operations have completed. + this.Delay(content_out_duration).FadeOut(); ApplyToBackground(b => b.IgnoreUserSettings.Value = true); @@ -357,6 +368,7 @@ private void contentIn() content.FadeInFromZero(400); content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); + lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint); ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint)); } @@ -366,8 +378,9 @@ private void contentOut() // Ensure the logo is no longer tracking before we scale the content content.StopTracking(); - content.ScaleTo(0.7f, 300, Easing.InQuint); - content.FadeOut(250); + content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint); + content.FadeOut(content_out_duration, Easing.OutQuint); + lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration); } private void pushWhenLoaded() @@ -394,7 +407,7 @@ private void pushWhenLoaded() this.Delay(extraDelay).Schedule(contentOut); - TransformSequence pushSequence = this.Delay(250 + extraDelay); + TransformSequence pushSequence = this.Delay(250 + extraDelay + content_out_duration); // only show if the warning was created (i.e. the beatmap needs it) // and this is not a restart of the map (the warning expires after first load). @@ -413,6 +426,11 @@ private void pushWhenLoaded() }) .Delay(EpilepsyWarning.FADE_DURATION); } + else + { + // This goes hand-in-hand with the restoration of low pass filter in contentOut(). + this.TransformBindableTo(volumeAdjustment, 0, content_out_duration, Easing.OutCubic); + } pushSequence.Schedule(() => { diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs index 8a4acacb249f..26887327cddf 100755 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs @@ -14,7 +14,7 @@ private void load(OsuColour colours) { Nub.AccentColour = colours.Yellow; Nub.GlowingAccentColour = colours.YellowLighter; - Nub.GlowColour = colours.YellowDarker; + Nub.GlowColour = colours.YellowDark; } } } diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs index c8e281195a2a..216e46d429dc 100755 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs @@ -29,7 +29,7 @@ private void load(OsuColour colours) AccentColour = colours.Yellow; Nub.AccentColour = colours.Yellow; Nub.GlowingAccentColour = colours.YellowLighter; - Nub.GlowColour = colours.YellowDarker; + Nub.GlowColour = colours.YellowDark; } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 10e131626a73..7cc4e49b18fa 100755 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -426,7 +426,7 @@ private void workingBeatmapChanged(ValueChangedEvent e) { if (e.NewValue is DummyWorkingBeatmap || !this.IsCurrentScreen()) return; - Logger.Log($"working beatmap updated to {e.NewValue}"); + Logger.Log($"Song select working beatmap updated to {e.NewValue}"); if (!Carousel.SelectBeatmap(e.NewValue.BeatmapInfo, false)) { diff --git a/osu.Game/Skinning/ISkinSource.cs b/osu.Game/Skinning/ISkinSource.cs index ba3e2bf6ad1f..a5ed0fc9900f 100644 --- a/osu.Game/Skinning/ISkinSource.cs +++ b/osu.Game/Skinning/ISkinSource.cs @@ -12,6 +12,9 @@ namespace osu.Game.Skinning /// public interface ISkinSource : ISkin { + /// + /// Fired whenever a source change occurs, signalling that consumers should re-query as required. + /// event Action SourceChanged; /// diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index a12defe87e0e..0c9a82074f88 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -16,7 +16,6 @@ public class LegacyScoreCounter : GameplayScoreCounter, ISkinnableDrawable public bool UsesFixedAnchor { get; set; } public LegacyScoreCounter() - : base(6) { Anchor = Anchor.TopRight; Origin = Anchor.TopRight; diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index f5a7788359f6..b8847947390e 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -58,10 +58,8 @@ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnl return base.CreateChildDependencies(parent); } - protected override void OnSourceChanged() + protected override void RefreshSources() { - ResetSources(); - // Populate a local list first so we can adjust the returned order as we go. var sources = new List(); @@ -91,8 +89,7 @@ protected override void OnSourceChanged() else sources.Add(rulesetResourcesSkin); - foreach (var skin in sources) - AddSource(skin); + SetSources(sources); } protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index ada6e4b7882a..c8e4c2c7b643 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -40,10 +41,12 @@ public class SkinProvidingContainer : Container, ISkinSource protected virtual bool AllowColourLookup => true; + private readonly object sourceSetLock = new object(); + /// /// A dictionary mapping each source to a wrapper which handles lookup allowances. /// - private readonly List<(ISkin skin, DisableableSkinSource wrapped)> skinSources = new List<(ISkin, DisableableSkinSource)>(); + private (ISkin skin, DisableableSkinSource wrapped)[] skinSources = Array.Empty<(ISkin skin, DisableableSkinSource wrapped)>(); /// /// Constructs a new initialised with a single skin source. @@ -52,7 +55,7 @@ public SkinProvidingContainer([CanBeNull] ISkin skin) : this() { if (skin != null) - AddSource(skin); + SetSources(new[] { skin }); } /// @@ -168,49 +171,42 @@ public IBindable GetConfig(TLookup lookup) } /// - /// Add a new skin to this provider. Will be added to the end of the lookup order precedence. + /// Replace the sources used for lookups in this container. /// - /// The skin to add. - protected void AddSource(ISkin skin) + /// + /// This does not implicitly fire a event. Consider calling if required. + /// + /// The new sources. + protected void SetSources(IEnumerable sources) { - skinSources.Add((skin, new DisableableSkinSource(skin, this))); - - if (skin is ISkinSource source) - source.SourceChanged += TriggerSourceChanged; - } - - /// - /// Remove a skin from this provider. - /// - /// The skin to remove. - protected void RemoveSource(ISkin skin) - { - if (skinSources.RemoveAll(s => s.skin == skin) == 0) - return; + lock (sourceSetLock) + { + foreach (var skin in skinSources) + { + if (skin.skin is ISkinSource source) + source.SourceChanged -= TriggerSourceChanged; + } - if (skin is ISkinSource source) - source.SourceChanged -= TriggerSourceChanged; - } + skinSources = sources.Select(skin => (skin, new DisableableSkinSource(skin, this))).ToArray(); - /// - /// Clears all skin sources. - /// - protected void ResetSources() - { - foreach (var i in skinSources.ToArray()) - RemoveSource(i.skin); + foreach (var skin in skinSources) + { + if (skin.skin is ISkinSource source) + source.SourceChanged += TriggerSourceChanged; + } + } } /// - /// Invoked when any source has changed (either or a source registered via ). + /// Invoked after any consumed source change, before the external event is fired. /// This is also invoked once initially during to ensure sources are ready for children consumption. /// - protected virtual void OnSourceChanged() { } + protected virtual void RefreshSources() { } protected void TriggerSourceChanged() { // Expose to implementations, giving them a chance to react before notifying external consumers. - OnSourceChanged(); + RefreshSources(); SourceChanged?.Invoke(); } diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs index e7125bb0342a..20c2fcc07591 100644 --- a/osu.Game/Skinning/SkinnableTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetContainer.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -22,6 +23,8 @@ public class SkinnableTargetContainer : SkinReloadableDrawable, ISkinnableTarget public bool ComponentsLoaded { get; private set; } + private CancellationTokenSource cancellationSource; + public SkinnableTargetContainer(SkinnableTarget target) { Target = target; @@ -38,6 +41,9 @@ public void Reload() content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer; + cancellationSource?.Cancel(); + cancellationSource = null; + if (content != null) { LoadComponentAsync(content, wrapper => @@ -45,7 +51,7 @@ public void Reload() AddInternal(wrapper); components.AddRange(wrapper.Children.OfType()); ComponentsLoaded = true; - }); + }, (cancellationSource = new CancellationTokenSource()).Token); } else ComponentsLoaded = true; diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs new file mode 100644 index 000000000000..f7b747163439 --- /dev/null +++ b/osu.Game/Stores/RealmFileStore.cs @@ -0,0 +1,116 @@ +// 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.IO; +using System.Linq; +using osu.Framework.Extensions; +using osu.Framework.IO.Stores; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Database; +using osu.Game.Models; +using Realms; + +#nullable enable + +namespace osu.Game.Stores +{ + /// + /// Handles the storing of files to the file system (and database) backing. + /// + [ExcludeFromDynamicCompile] + public class RealmFileStore + { + private readonly RealmContextFactory realmFactory; + + public readonly IResourceStore Store; + + public readonly Storage Storage; + + public RealmFileStore(RealmContextFactory realmFactory, Storage storage) + { + this.realmFactory = realmFactory; + + Storage = storage.GetStorageForDirectory(@"files"); + Store = new StorageBackedResourceStore(Storage); + } + + /// + /// Add a new file to the game-wide database, copying it to permanent storage if not already present. + /// + /// The file data stream. + /// The realm instance to add to. Should already be in a transaction. + /// + public RealmFile Add(Stream data, Realm realm) + { + string hash = data.ComputeSHA2Hash(); + + var existing = realm.Find(hash); + + var file = existing ?? new RealmFile { Hash = hash }; + + if (!checkFileExistsAndMatchesHash(file)) + copyToStore(file, data); + + if (!file.IsManaged) + realm.Add(file); + + return file; + } + + private void copyToStore(RealmFile file, Stream data) + { + data.Seek(0, SeekOrigin.Begin); + + using (var output = Storage.GetStream(file.StoragePath, FileAccess.Write)) + data.CopyTo(output); + + data.Seek(0, SeekOrigin.Begin); + } + + private bool checkFileExistsAndMatchesHash(RealmFile file) + { + string path = file.StoragePath; + + // we may be re-adding a file to fix missing store entries. + if (!Storage.Exists(path)) + return false; + + // even if the file already exists, check the existing checksum for safety. + using (var stream = Storage.GetStream(path)) + return stream.ComputeSHA2Hash() == file.Hash; + } + + public void Cleanup() + { + var realm = realmFactory.Context; + + // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. + using (var transaction = realm.BeginWrite()) + { + // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707) + var files = realm.All().ToList(); + + foreach (var file in files) + { + if (file.BacklinksCount > 0) + continue; + + try + { + Storage.Delete(file.StoragePath); + realm.Remove(file); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete databased file {file.Hash}"); + } + } + + transaction.Commit(); + } + } + } +} diff --git a/osu.Game/Stores/RealmRulesetStore.cs b/osu.Game/Stores/RealmRulesetStore.cs new file mode 100644 index 000000000000..27eb5d797fb9 --- /dev/null +++ b/osu.Game/Stores/RealmRulesetStore.cs @@ -0,0 +1,263 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using osu.Framework; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Models; +using osu.Game.Rulesets; + +#nullable enable + +namespace osu.Game.Stores +{ + public class RealmRulesetStore : IDisposable + { + private readonly RealmContextFactory realmFactory; + + private const string ruleset_library_prefix = @"osu.Game.Rulesets"; + + private readonly Dictionary loadedAssemblies = new Dictionary(); + + /// + /// All available rulesets. + /// + public IEnumerable AvailableRulesets => availableRulesets; + + private readonly List availableRulesets = new List(); + + public RealmRulesetStore(RealmContextFactory realmFactory, Storage? storage = null) + { + this.realmFactory = realmFactory; + + // On android in release configuration assemblies are loaded from the apk directly into memory. + // We cannot read assemblies from cwd, so should check loaded assemblies instead. + loadFromAppDomain(); + + // This null check prevents Android from attempting to load the rulesets from disk, + // as the underlying path "AppContext.BaseDirectory", despite being non-nullable, it returns null on android. + // See https://github.com/xamarin/xamarin-android/issues/3489. + if (RuntimeInfo.StartupDirectory != null) + loadFromDisk(); + + // the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory. + // It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail + // to load as unable to locate the game core assembly. + AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly; + + var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets"); + if (rulesetStorage != null) + loadUserRulesets(rulesetStorage); + + addMissingRulesets(); + } + + /// + /// Retrieve a ruleset using a known ID. + /// + /// The ruleset's internal ID. + /// A ruleset, if available, else null. + public IRulesetInfo? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id); + + /// + /// Retrieve a ruleset using a known short name. + /// + /// The ruleset's short name. + /// A ruleset, if available, else null. + public IRulesetInfo? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName); + + private Assembly? resolveRulesetDependencyAssembly(object? sender, ResolveEventArgs args) + { + var asm = new AssemblyName(args.Name); + + // the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies. + // this attempts resolving the ruleset dependencies on game core and framework assemblies by returning assemblies with the same assembly name + // already loaded in the AppDomain. + var domainAssembly = AppDomain.CurrentDomain.GetAssemblies() + // Given name is always going to be equally-or-more qualified than the assembly name. + .Where(a => + { + string? name = a.GetName().Name; + if (name == null) + return false; + + return args.Name.Contains(name, StringComparison.Ordinal); + }) + // Pick the greatest assembly version. + .OrderByDescending(a => a.GetName().Version) + .FirstOrDefault(); + + if (domainAssembly != null) + return domainAssembly; + + return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName); + } + + private void addMissingRulesets() + { + realmFactory.Context.Write(realm => + { + var rulesets = realm.All(); + + List instances = loadedAssemblies.Values + .Select(r => Activator.CreateInstance(r) as Ruleset) + .Where(r => r != null) + .Select(r => r.AsNonNull()) + .ToList(); + + // add all legacy rulesets first to ensure they have exclusive choice of primary key. + foreach (var r in instances.Where(r => r is ILegacyRuleset)) + { + if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.ID) == null) + realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.ID)); + } + + // add any other rulesets which have assemblies present but are not yet in the database. + foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) + { + if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) + { + var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName); + + if (existingSameShortName != null) + { + // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName. + // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one. + // in such cases, update the instantiation info of the existing entry to point to the new one. + existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo; + } + else + realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.ID)); + } + } + + List detachedRulesets = new List(); + + // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage. + foreach (var r in rulesets) + { + try + { + var type = Type.GetType(r.InstantiationInfo); + + if (type == null) + throw new InvalidOperationException(@"Type resolution failure."); + + var rInstance = (Activator.CreateInstance(type) as Ruleset)?.RulesetInfo; + + if (rInstance == null) + throw new InvalidOperationException(@"Instantiation failure."); + + r.Name = rInstance.Name; + r.ShortName = rInstance.ShortName; + r.InstantiationInfo = rInstance.InstantiationInfo; + r.Available = true; + + detachedRulesets.Add(r.Clone()); + } + catch (Exception ex) + { + r.Available = false; + Logger.Log($"Could not load ruleset {r}: {ex.Message}"); + } + } + + availableRulesets.AddRange(detachedRulesets); + }); + } + + private void loadFromAppDomain() + { + foreach (var ruleset in AppDomain.CurrentDomain.GetAssemblies()) + { + string? rulesetName = ruleset.GetName().Name; + + if (rulesetName == null) + continue; + + if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || rulesetName.Contains(@"Tests")) + continue; + + addRuleset(ruleset); + } + } + + private void loadUserRulesets(Storage rulesetStorage) + { + var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll"); + + foreach (var ruleset in rulesets.Where(f => !f.Contains(@"Tests"))) + loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset)); + } + + private void loadFromDisk() + { + try + { + var files = Directory.GetFiles(RuntimeInfo.StartupDirectory, @$"{ruleset_library_prefix}.*.dll"); + + foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests"))) + loadRulesetFromFile(file); + } + catch (Exception e) + { + Logger.Error(e, $"Could not load rulesets from directory {RuntimeInfo.StartupDirectory}"); + } + } + + private void loadRulesetFromFile(string file) + { + var filename = Path.GetFileNameWithoutExtension(file); + + if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename)) + return; + + try + { + addRuleset(Assembly.LoadFrom(file)); + } + catch (Exception e) + { + Logger.Error(e, $"Failed to load ruleset {filename}"); + } + } + + private void addRuleset(Assembly assembly) + { + if (loadedAssemblies.ContainsKey(assembly)) + return; + + // the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799). + // as a failsafe, also compare by FullName. + if (loadedAssemblies.Any(a => a.Key.FullName == assembly.FullName)) + return; + + try + { + loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset))); + } + catch (Exception e) + { + Logger.Error(e, $"Failed to add ruleset {assembly}"); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly; + } + } +} diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 64f1ee4a7a2a..6d63525011de 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -14,6 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; +using osu.Game.IO.Serialization; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -109,6 +110,8 @@ private ConvertResult convert(string name, Mod[] mods) { var beatmap = GetBeatmap(name); + string beforeConversion = beatmap.Serialize(); + var converterResult = new Dictionary>(); var working = new ConversionWorkingBeatmap(beatmap) @@ -122,6 +125,10 @@ private ConvertResult convert(string name, Mod[] mods) working.GetPlayableBeatmap(CreateRuleset().RulesetInfo, mods); + string afterConversion = beatmap.Serialize(); + + Assert.AreEqual(beforeConversion, afterConversion, "Conversion altered original beatmap"); + return new ConvertResult { Mappings = converterResult.Select(r => diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 34393fba7db9..c2e9892735a6 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -5,7 +5,6 @@ using osu.Framework.Bindables; using osu.Framework.Input.Events; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Screens.Edit; namespace osu.Game.Tests.Visual @@ -23,7 +22,7 @@ public abstract class EditorClockTestScene : OsuManualInputManagerTestScene protected EditorClockTestScene() { - Clock = new EditorClock(new ControlPointInfo(), BeatDivisor) { IsCoupled = false }; + Clock = new EditorClock(new Beatmap(), BeatDivisor) { IsCoupled = false }; } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -44,7 +43,7 @@ private void load() private void beatmapChanged(ValueChangedEvent e) { - Clock.ControlPointInfo = e.NewValue.Beatmap.ControlPointInfo; + Clock.Beatmap = e.NewValue.Beatmap; Clock.ChangeSource(e.NewValue.Track); Clock.ProcessFrame(); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 2c0ca0b872da..5e4e5942d93d 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -53,7 +53,8 @@ public TestMultiplayerClient(TestRequestHandlingMultiplayerRoomManager roomManag public MultiplayerRoomUser AddUser(User user, bool markAsPlaying = false) { var roomUser = new MultiplayerRoomUser(user.Id) { User = user }; - ((IMultiplayerClient)this).UserJoined(roomUser); + + addUser(roomUser); if (markAsPlaying) PlayingUserIds.Add(user.Id); @@ -61,7 +62,15 @@ public MultiplayerRoomUser AddUser(User user, bool markAsPlaying = false) return roomUser; } - public void AddNullUser() => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(TestUserLookupCache.NULL_USER_ID)); + public void AddNullUser() => addUser(new MultiplayerRoomUser(TestUserLookupCache.NULL_USER_ID)); + + private void addUser(MultiplayerRoomUser user) + { + ((IMultiplayerClient)this).UserJoined(user).Wait(); + + // We want the user to be immediately available for testing, so force a scheduler update to run the update-bound continuation. + Scheduler.Update(); + } public void RemoveUser(User user) { diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index c025cf85c73c..6a11bd3feac2 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -22,6 +22,7 @@ using osu.Game.Screens; using osu.Game.Screens.Menu; using osuTK.Graphics; +using IntroSequence = osu.Game.Configuration.IntroSequence; namespace osu.Game.Tests.Visual { @@ -77,9 +78,11 @@ public void TearDownSteps() protected void CreateGame() { - AddGame(Game = new TestOsuGame(LocalStorage, API)); + AddGame(Game = CreateTestGame()); } + protected virtual TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API); + protected void PushAndConfirm(Func newScreen) { Screen screen = null; @@ -126,14 +129,16 @@ public class TestOsuGame : OsuGame public new Bindable> SelectedMods => base.SelectedMods; - // if we don't do this, when running under nUnit the version that gets populated is that of nUnit. + // if we don't apply these changes, when running under nUnit the version that gets populated is that of nUnit. + public override Version AssemblyVersion => new Version(0, 0); public override string Version => "test game"; protected override Loader CreateLoader() => new TestLoader(); public new void PerformFromScreen(Action action, IEnumerable validScreens = null) => base.PerformFromScreen(action, validScreens); - public TestOsuGame(Storage storage, IAPIProvider api) + public TestOsuGame(Storage storage, IAPIProvider api, string[] args = null) + : base(args) { Storage = storage; API = api; @@ -142,6 +147,9 @@ public TestOsuGame(Storage storage, IAPIProvider api) protected override void LoadComplete() { base.LoadComplete(); + + LocalConfig.SetValue(OsuSetting.IntroSequence, IntroSequence.Circles); + API.Login("Rhythm Champion", "osu!"); Dependencies.Get().SetValue(Static.MutedAudioNotificationShownOnce, true); diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index 42cf826bd476..b2f5b1754f3e 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -24,24 +25,25 @@ protected PlacementBlueprintTestScene() base.Content.Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock()))); } - [BackgroundDependencyLoader] - private void load() - { - Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = 2; - } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs(new EditorClock()); - var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + var playable = GetPlayableBeatmap(); dependencies.CacheAs(new EditorBeatmap(playable)); return dependencies; } + protected virtual IBeatmap GetPlayableBeatmap() + { + var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + playable.Difficulty.CircleSize = 2; + return playable; + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index cfa2d2632906..cc2608a9d55a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,11 +36,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/osu.iOS.props b/osu.iOS.props index edce9d27fecd..92abab036ae3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,8 +70,8 @@ - - + + @@ -93,7 +93,7 @@ - +