diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index bc2626d3d6..9e11ab6663 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -53,6 +53,7 @@ jobs: diffcalc: name: Run runs-on: self-hosted + timeout-minutes: 1440 if: needs.metadata.outputs.continue == 'yes' needs: metadata strategy: diff --git a/osu.Android.props b/osu.Android.props index 8fad10d247..5a0e7479fa 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,11 +51,11 @@ - - + + - + diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index dcb88efeb6..e2b40e9dc6 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -140,10 +140,10 @@ namespace osu.Desktop switch (activity) { case UserActivity.InGame game: - return game.Beatmap.ToString(); + return game.BeatmapInfo.ToString(); case UserActivity.Editing edit: - return edit.Beatmap.ToString(); + return edit.BeatmapInfo.ToString(); case UserActivity.InLobby lobby: return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 540f02580f..f291bfed13 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public IEnumerable CaughtObjects => this.ChildrenOfType(); - public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty) + public TestCatcher(DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty) : base(droppedObjectTarget, difficulty) { } @@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Catch.Tests public class TestKiaiFruit : Fruit { - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 6abfbdbe21..7cae9b18b9 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests private ScheduledDelegate addManyFruit; - private BeatmapDifficulty beatmapDifficulty; + private IBeatmapDifficultyInfo beatmapDifficulty; public TestSceneCatcherArea() { @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Tests private class TestCatcherArea : CatcherArea { - public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) + public TestCatcherArea(IBeatmapDifficultyInfo beatmapDifficulty) { var droppedObjectContainer = new DroppedObjectContainer(); Add(droppedObjectContainer); diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 82d76252d2..5b1f613f8d 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty return new CatchDifficultyAttributes { Mods = mods, Skills = skills }; // this is the same as osu!, so there's potential to share the implementation... maybe - double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; return new CatchDifficultyAttributes { diff --git a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs index 8c9f292aa9..046ba0ebce 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Edit public class CatchEditorPlayfield : CatchPlayfield { // TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen. - public CatchEditorPlayfield(BeatmapDifficulty difficulty) + public CatchEditorPlayfield(IBeatmapDifficultyInfo difficulty) : base(difficulty) { } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index d43e6f1c8b..ee10cf9711 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -128,11 +128,11 @@ namespace osu.Game.Rulesets.Catch.Objects /// public int RandomSeed => (int)StartTime; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index a8ad34fcbe..0d6925a83d 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Objects /// public double SpanDuration => Duration / this.SpanCount(); - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 1e20643a08..df32d917ce 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Catch.UI internal CatcherArea CatcherArea { get; private set; } - private readonly BeatmapDifficulty difficulty; + private readonly IBeatmapDifficultyInfo difficulty; - public CatchPlayfield(BeatmapDifficulty difficulty) + public CatchPlayfield(IBeatmapDifficultyInfo difficulty) { this.difficulty = difficulty; } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 5cd85aac56..3745099010 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Catch.UI private readonly DrawablePool caughtBananaPool; private readonly DrawablePool caughtDropletPool; - public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null) + public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty = null) { this.droppedObjectTarget = droppedObjectTarget; @@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Calculates the scale of the catcher based off the provided beatmap difficulty. /// - private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); /// /// Calculates the width of the area used for attempting catches in gameplay. @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Catch.UI /// Calculates the width of the area used for attempting catches in gameplay. /// /// The beatmap difficulty. - public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty)); + public static float CalculateCatchWidth(IBeatmapDifficultyInfo difficulty) => CalculateCatchWidth(calculateScale(difficulty)); /// /// Determine if this catcher can catch a in the current position. diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 8b6a074426..ba6e9224c9 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.UI : base(ruleset, beatmap, mods) { Direction.Value = ScrollingDirection.Down; - TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); + TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 26393c8edb..01e44ef9e4 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps originalTargetColumns = TargetColumns; } - public static int GetColumnCountForNonConvert(BeatmapInfo beatmap) + public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo) { - var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize); + var roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize); return (int)Math.Max(1, roundedCircleSize); } @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { - BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty; + IBeatmapDifficultyInfo difficulty = original.BeatmapInfo.BaseDifficulty; 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/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index e643b82271..d65e78bb49 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 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (drainTime == 0) drainTime = 10000; - BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; + IBeatmapDifficultyInfo difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; 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 a7a6677b68..fc29eadedc 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty 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), - ScoreMultiplier = getScoreMultiplier(beatmap, mods), + ScoreMultiplier = getScoreMultiplier(mods), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), Skills = skills }; @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { - new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns) + new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns) }; protected override Mod[] DifficultyAdjustmentMods @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty } } - private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods) + private double getScoreMultiplier(Mod[] mods) { double scoreMultiplier = 1; @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty } } - var maniaBeatmap = (ManiaBeatmap)beatmap; + var maniaBeatmap = (ManiaBeatmap)Beatmap; int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns; if (diff > 0) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index d9a278ef29..0290230490 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -13,9 +13,9 @@ namespace osu.Game.Rulesets.Mania { private FilterCriteria.OptionalRange keys; - public bool Matches(BeatmapInfo beatmap) + public bool Matches(BeatmapInfo beatmapInfo) { - return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap))); + return !keys.HasFilter || (beatmapInfo.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo))); } public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 43e876b7aa..c1937af7e4 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// private double tickSpacing = 50; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs index 6a3f168ee1..787807a8ea 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks public class CheckTooShortSpinnersTest { private CheckTooShortSpinners check; - private BeatmapDifficulty difficulty; + private IBeatmapDifficultyInfo difficulty; [SetUp] public void Setup() @@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks assertTooShort(new List { spinnerHighOd }, difficultyHighOd); } - private void assertOk(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private void assertOk(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty); } - private void assertVeryShort(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private void assertVeryShort(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList(); @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort); } - private void assertTooShort(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private void assertTooShort(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList(); @@ -102,12 +102,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort); } - private BeatmapVerifierContext getContext(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private BeatmapVerifierContext getContext(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { var beatmap = new Beatmap { HitObjects = hitObjects, - BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty } + BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(beatmapDifficulty) } }; return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index 1ac3ad9194..ed9da36b05 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -4,13 +4,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -122,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4; private bool objectWithIncreasedVisibilityHasIndex(int index) - => Player.Mods.Value.OfType().Single().FirstObject == Player.ChildrenOfType().Single().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/Resources/special-skin/hitcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png index a9b2d95d88..8e50cd0335 100755 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini new file mode 100644 index 0000000000..49ac2cf80d --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini @@ -0,0 +1,3 @@ +[General] +Version: latest +HitCircleOverlayAboveNumber: 0 diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs index 10d9d7ffde..79150a1941 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Position = new Vector2(100, 300), }, - accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) + accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index f9dc9abd75..41d9bf7132 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -17,6 +17,7 @@ using osu.Framework.Testing.Input; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; @@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests public class TestSceneGameplayCursor : OsuSkinnableTestScene { [Cached] - private GameplayBeatmap gameplayBeatmap; + private GameplayState gameplayState; private OsuCursorContainer lastContainer; @@ -40,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Tests public TestSceneGameplayCursor() { - gameplayBeatmap = new GameplayBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); + var ruleset = new OsuRuleset(); + gameplayState = new GameplayState(CreateBeatmap(ruleset.RulesetInfo), ruleset, Array.Empty()); AddStep("change background colour", () => { @@ -57,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddSliderStep("circle size", 0f, 10f, 0f, val => { config.SetValue(OsuSetting.AutoCursorSize, true); - gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val; + gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = val; Scheduler.AddOnce(() => loadContent(false)); }); @@ -73,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestSizing(int circleSize, float userScale) { AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale)); - AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize); + AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize); AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true)); AddStep("load content", () => loadContent()); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 77a68b714b..cfce80a2b2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -452,7 +452,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestSpinner : Spinner { - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); SpinsRequired = 1; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 177a4f50a1..1b85e0efde 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestSpinner : Spinner { - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); SpinsRequired = 1; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 4c8d0b2ce6..a8f10f44dc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty 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)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; 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) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 210d5e0403..b0c655b106 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Mods #region Reduce AR (IApplicableToDifficulty) - public void ReadFromDifficulty(BeatmapDifficulty difficulty) + public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) { } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 36629fa41e..7c45b2bc07 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -122,11 +122,11 @@ namespace osu.Game.Rulesets.Osu.Objects }); } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index c4420b1e87..1d2666f46b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Objects Path.Version.ValueChanged += _ => updateNestedPositions(); } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index a6aed2c00e..f893559548 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects public double SpanDuration => slider.SpanDuration; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 725dbe81fb..e7e64954e9 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects public int SpanIndex { get; set; } public double SpanStartTime { get; set; } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 194aa640f9..f85dc0d391 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects /// public int MaximumBonusSpins { get; protected set; } = 1; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects double secondsDuration = Duration / 1000; - double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); + double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond); MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs index c2db5f3f82..611ddd08eb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private OsuPlayfield playfield { get; set; } [Resolved(canBeNull: true)] - private GameplayBeatmap gameplayBeatmap { get; set; } + private GameplayState gameplayState { get; set; } [BackgroundDependencyLoader] private void load(ISkinSource skin, OsuColour colours) @@ -75,12 +75,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void Update() { - if (playfield == null || gameplayBeatmap == null) return; + if (playfield == null || gameplayState == null) return; DrawableHitObject kiaiHitObject = null; // Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary. - if (gameplayBeatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode) + if (gameplayState.Beatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode) kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking); kiaiSpewer.Active.Value = kiaiHitObject != null; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 3afd814174..d1c9b1bf92 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -35,8 +35,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Drawable hitCircleSprite; - protected Drawable HitCircleOverlay { get; private set; } + protected Container OverlayLayer { get; private set; } + private Drawable hitCircleOverlay; private SkinnableSpriteText hitCircleText; private readonly Bindable accentColour = new Bindable(); @@ -78,17 +79,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - HitCircleOverlay = new KiaiFlashingSprite + OverlayLayer = new Container { - Texture = overlayTexture, Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, + Child = hitCircleOverlay = new KiaiFlashingSprite + { + Texture = overlayTexture, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } }; if (hasNumber) { - AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText + OverlayLayer.Add(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, @@ -102,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; if (overlayAboveNumber) - ChangeInternalChildDepth(HitCircleOverlay, float.MinValue); + OverlayLayer.ChangeChildDepth(hitCircleOverlay, float.MinValue); accentColour.BindTo(drawableObject.AccentColour); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); @@ -147,8 +153,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out); hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - HitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out); - HitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + hitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out); + hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); if (hasNumber) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs index 13ba42ba50..7de2b8c7fa 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy [Resolved(canBeNull: true)] private DrawableHitObject drawableHitObject { get; set; } - private Drawable proxiedHitCircleOverlay; + private Drawable proxiedOverlayLayer; public LegacySliderHeadHitCircle() : base("sliderstartcircle") @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void LoadComplete() { base.LoadComplete(); - proxiedHitCircleOverlay = HitCircleOverlay.CreateProxy(); + proxiedOverlayLayer = OverlayLayer.CreateProxy(); if (drawableHitObject != null) { @@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private void onHitObjectApplied(DrawableHitObject drawableObject) { - Debug.Assert(proxiedHitCircleOverlay.Parent == null); + Debug.Assert(proxiedOverlayLayer.Parent == null); // see logic in LegacyReverseArrow. (drawableObject as DrawableSliderHead)?.DrawableSlider - .OverlayElementContainer.Add(proxiedHitCircleOverlay.With(d => d.Depth = float.MinValue)); + .OverlayElementContainer.Add(proxiedOverlayLayer.With(d => d.Depth = float.MinValue)); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 83bcc88e5f..cfe83d0106 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } [Resolved(canBeNull: true)] - private GameplayBeatmap beatmap { get; set; } + private GameplayState state { get; set; } [Resolved] private OsuConfigManager config { get; set; } @@ -96,10 +96,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { float scale = userCursorScale.Value; - if (autoCursorScale.Value && beatmap != null) + 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(beatmap.BeatmapInfo.BaseDifficulty.CircleSize); + scale *= GetScaleForCircleSize(state.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize); } cursorScale.Value = scale; diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 9b73e644c5..3b5b972c01 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps case IHasDuration endTimeData: { - double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; + double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; yield return new Swell { @@ -193,9 +193,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty { - public TaikoMutliplierAppliedDifficulty(BeatmapDifficulty difficulty) + public TaikoMutliplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty) { - difficulty.CopyTo(this); + CopyFrom(difficulty); + SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index b0634295d0..0318e32991 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Taiko.Objects private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index f7a1d130eb..94cd411d7b 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring { base.ApplyBeatmap(beatmap); - hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); - hpMissMultiplier = BeatmapDifficulty.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.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); + hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); } protected override double GetHealthIncreaseFor(JudgementResult result) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs index 6fc59ea0e8..fa49242675 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs @@ -25,10 +25,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } [BackgroundDependencyLoader(true)] - private void load(GameplayBeatmap gameplayBeatmap) + private void load(GameplayState gameplayState) { - if (gameplayBeatmap != null) - ((IBindable)LastResult).BindTo(gameplayBeatmap.LastJudgementResult); + if (gameplayState != null) + ((IBindable)LastResult).BindTo(gameplayState.LastJudgementResult); } private bool passing; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 6a16f311bf..e1063e1071 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.UI } [BackgroundDependencyLoader(true)] - private void load(TextureStore textures, GameplayBeatmap gameplayBeatmap) + private void load(TextureStore textures, GameplayState gameplayState) { InternalChildren = new[] { @@ -49,8 +49,8 @@ namespace osu.Game.Rulesets.Taiko.UI animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail), }; - if (gameplayBeatmap != null) - ((IBindable)LastResult).BindTo(gameplayBeatmap.LastJudgementResult); + if (gameplayState != null) + ((IBindable)LastResult).BindTo(gameplayState.LastJudgementResult); } protected override void LoadComplete() diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index bcde899789..560e2ef894 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -149,5 +149,32 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[5]).LoopType); } } + + [Test] + public void TestDecodeLoopCount() + { + // all loop sequences in loop-count.osb have a total duration of 2000ms (fade in 0->1000ms, fade out 1000->2000ms). + const double loop_duration = 2000; + + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("loop-count.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + + // stable ensures that any loop command executes at least once, even if the loop count specified in the .osb is zero or negative. + StoryboardSprite zeroTimes = background.Elements.OfType().Single(s => s.Path == "zero-times.png"); + Assert.That(zeroTimes.EndTime, Is.EqualTo(1000 + loop_duration)); + + StoryboardSprite oneTime = background.Elements.OfType().Single(s => s.Path == "one-time.png"); + Assert.That(oneTime.EndTime, Is.EqualTo(4000 + loop_duration)); + + StoryboardSprite manyTimes = background.Elements.OfType().Single(s => s.Path == "many-times.png"); + Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + 40 * loop_duration)); + } + } } } diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index cba7f34ede..4cc71717ff 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - BeatmapSetInfo importedSet; + ILive importedSet; using (var stream = File.OpenRead(tempPath)) { @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing"); File.Delete(tempPath); - var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); deleteBeatmapSet(imported, osu); } @@ -172,8 +172,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // but contents doesn't, so existing should still be used. - Assert.IsTrue(imported.ID == importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID == importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -226,8 +226,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -278,8 +278,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -329,8 +329,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -570,8 +570,8 @@ namespace osu.Game.Tests.Beatmaps.IO var imported = await manager.Import(toImport); Assert.NotNull(imported); - Assert.AreEqual(null, imported.Beatmaps[0].OnlineBeatmapID); - Assert.AreEqual(null, imported.Beatmaps[1].OnlineBeatmapID); + Assert.AreEqual(null, imported.Value.Beatmaps[0].OnlineBeatmapID); + Assert.AreEqual(null, imported.Value.Beatmaps[1].OnlineBeatmapID); } finally { @@ -706,7 +706,7 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); - Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder"); + Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder"); } finally { @@ -759,8 +759,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); - Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored"); - Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder"); + Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored"); + Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder"); } finally { @@ -909,13 +909,13 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - var importedSet = await manager.Import(new ImportTask(temp)); + var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false); ensureLoaded(osu); waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); } public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) @@ -924,13 +924,13 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - var importedSet = await manager.Import(new ImportTask(temp)); + var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false); ensureLoaded(osu); waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); } private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) @@ -945,13 +945,13 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); } - private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap) + private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmapInfo) { return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2, - Beatmap = beatmap, - BeatmapInfoID = beatmap.ID + BeatmapInfo = beatmapInfo, + BeatmapInfoID = beatmapInfo.ID }, new ImportScoreTest.TestArchiveReader()); } diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs new file mode 100644 index 0000000000..245981cd9b --- /dev/null +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public class GeneralUsageTests : RealmTest + { + /// + /// Just test the construction of a new database works. + /// + [Test] + public void TestConstructRealm() + { + RunTestWithRealm((realmFactory, _) => { realmFactory.CreateContext().Refresh(); }); + } + + [Test] + public void TestBlockOperations() + { + RunTestWithRealm((realmFactory, _) => + { + using (realmFactory.BlockAllOperations()) + { + } + }); + } + + [Test] + public void TestBlockOperationsWithContention() + { + RunTestWithRealm((realmFactory, _) => + { + ManualResetEventSlim stopThreadedUsage = new ManualResetEventSlim(); + ManualResetEventSlim hasThreadedUsage = new ManualResetEventSlim(); + + Task.Factory.StartNew(() => + { + using (realmFactory.CreateContext()) + { + hasThreadedUsage.Set(); + + stopThreadedUsage.Wait(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler); + + hasThreadedUsage.Wait(); + + Assert.Throws(() => + { + using (realmFactory.BlockAllOperations()) + { + } + }); + + stopThreadedUsage.Set(); + }); + } + } +} diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs new file mode 100644 index 0000000000..576f901c1a --- /dev/null +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Nito.AsyncEx; +using NUnit.Framework; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Database; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public abstract class RealmTest + { + private static readonly TemporaryNativeStorage storage; + + static RealmTest() + { + storage = new TemporaryNativeStorage("realm-test"); + storage.DeleteDirectory(string.Empty); + } + + protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") + { + AsyncContext.Run(() => + { + var testStorage = storage.GetStorageForDirectory(caller); + + using (var realmFactory = new RealmContextFactory(testStorage, caller)) + { + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); + testAction(realmFactory, testStorage); + + 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 () => + { + 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(); + + Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); + realmFactory.Compact(); + Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); + } + }); + } + + private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory) + { + try + { + using (var stream = testStorage.GetStream(realmFactory.Filename)) + return stream?.Length ?? 0; + } + catch + { + // windows runs may error due to file still being open. + return 0; + } + } + } +} diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 8be74f1a7c..f10b11733e 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Database storage = new NativeStorage(directory.FullName); - realmContextFactory = new RealmContextFactory(storage); + realmContextFactory = new RealmContextFactory(storage, "test"); keyBindingStore = new RealmKeyBindingStore(realmContextFactory); } @@ -53,9 +53,9 @@ namespace osu.Game.Tests.Database private int queryCount(GlobalAction? match = null) { - using (var usage = realmContextFactory.GetForRead()) + using (var realm = realmContextFactory.CreateContext()) { - var results = usage.Realm.All(); + var results = realm.All(); if (match.HasValue) results = results.Where(k => k.ActionInt == (int)match.Value); return results.Count(); @@ -69,26 +69,24 @@ namespace osu.Game.Tests.Database keyBindingStore.Register(testContainer, Enumerable.Empty()); - using (var primaryUsage = realmContextFactory.GetForRead()) + using (var primaryRealm = realmContextFactory.CreateContext()) { - var backBinding = primaryUsage.Realm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); + var backBinding = primaryRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); var tsr = ThreadSafeReference.Create(backBinding); - using (var usage = realmContextFactory.GetForWrite()) + using (var threadedContext = realmContextFactory.CreateContext()) { - var binding = usage.Realm.ResolveReference(tsr); - binding.KeyCombination = new KeyCombination(InputKey.BackSpace); - - usage.Commit(); + var binding = threadedContext.ResolveReference(tsr); + threadedContext.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); } Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); // check still correct after re-query. - backBinding = primaryUsage.Realm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); + backBinding = primaryRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); } } diff --git a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs index dab4825919..9926acf772 100644 --- a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs +++ b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Localisation Title = "Romanised title", TitleUnicode = "Unicode Title" }; - var romanisableString = metadata.ToRomanisableString(); + var romanisableString = metadata.GetDisplayTitleRomanisable(); Assert.AreEqual(metadata.ToString(), romanisableString.Romanised); Assert.AreEqual($"{metadata.ArtistUnicode} - {metadata.TitleUnicode}", romanisableString.Original); @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Localisation Artist = "Romanised Artist", Title = "Romanised title" }; - var romanisableString = metadata.ToRomanisableString(); + var romanisableString = metadata.GetDisplayTitleRomanisable(); Assert.AreEqual(romanisableString.Romanised, romanisableString.Original); } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 8ff2743b6a..ed86daf8b6 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -239,7 +239,7 @@ namespace osu.Game.Tests.NonVisual.Filtering match = shouldMatch; } - public bool Matches(BeatmapInfo beatmap) => match; + public bool Matches(BeatmapInfo beatmapInfo) => match; public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false; } } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index a55bdd2df8..df42c70c87 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -256,7 +256,7 @@ namespace osu.Game.Tests.NonVisual.Filtering { public string CustomValue { get; set; } - public bool Matches(BeatmapInfo beatmap) => true; + public bool Matches(BeatmapInfo beatmapInfo) => true; public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) { diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs index aa29d76843..91c6b6c008 100644 --- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs +++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Online AddAssert("response event fired", () => response != null); - AddAssert("request has response", () => request.Result == response); + AddAssert("request has response", () => request.Response == response); } [Test] diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 7e7e5ebc45..79767bc671 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -156,20 +156,49 @@ namespace osu.Game.Tests.Online { public TaskCompletionSource AllowImport = new TaskCompletionSource(); - public Task CurrentImportTask { get; private set; } + public Task> CurrentImportTask { get; private set; } - protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) - => new TestDownloadRequest(set); - - public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) - : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap, performOnlineLookups) + public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) + : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) { } - public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) { - await AllowImport.Task.ConfigureAwait(false); - return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false); + return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host); + } + + protected override BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host) + { + return new TestBeatmapModelDownloader(modelManager, api, host); + } + + internal class TestBeatmapModelDownloader : BeatmapModelDownloader + { + public TestBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost) + : base(modelManager, apiProvider, gameHost) + { + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) + => new TestDownloadRequest(set); + } + + internal class TestBeatmapModelManager : BeatmapModelManager + { + private readonly TestBeatmapManager testBeatmapManager; + + public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) + : base(storage, databaseContextFactory, rulesetStore, gameHost) + { + this.testBeatmapManager = testBeatmapManager; + } + + public override async Task> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + await testBeatmapManager.AllowImport.Task.ConfigureAwait(false); + return await (testBeatmapManager.CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false); + } } } diff --git a/osu.Game.Tests/Resources/loop-count.osb b/osu.Game.Tests/Resources/loop-count.osb new file mode 100644 index 0000000000..ec75e85ef1 --- /dev/null +++ b/osu.Game.Tests/Resources/loop-count.osb @@ -0,0 +1,15 @@ +osu file format v14 + +[Events] +Sprite,Background,TopCentre,"zero-times.png",320,240 + L,1000,0 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 +Sprite,Background,TopCentre,"one-time.png",320,240 + L,4000,1 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 +Sprite,Background,TopCentre,"many-times.png",320,240 + L,9000,40 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index cd7d744f53..2cd02329b7 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Scores.IO var beatmapManager = osu.Dependencies.Get(); var scoreManager = osu.Dependencies.Get(); - beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID))); + beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID))); Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true)); var secondImport = await LoadScoreIntoOsu(osu, imported); @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Scores.IO { var beatmapManager = osu.Dependencies.Get(); - score.Beatmap ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + score.BeatmapInfo ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); score.Ruleset ??= new OsuRuleset().RulesetInfo; var scoreManager = osu.Dependencies.Get(); diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 7a9fc20426..b2600bb887 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -196,7 +196,7 @@ namespace osu.Game.Tests.Skins.IO private async Task loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) { var skinManager = osu.Dependencies.Get(); - return await skinManager.Import(archive); + return (await skinManager.Import(archive)).Value; } } } diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index eff430ac25..f03cda1489 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Skins private void load() { var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result; - beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]); + beatmap = beatmaps.GetWorkingBeatmap(imported.Value.Beatmaps[0]); beatmap.LoadTrack(); } diff --git a/osu.Game.Tests/Skins/TestSceneSkinResources.cs b/osu.Game.Tests/Skins/TestSceneSkinResources.cs index 107a96292f..10f1ab31df 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinResources.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Skins private void load() { var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).Result; - skin = skins.GetSkin(imported); + skin = skins.GetSkin(imported.Value); } [Test] diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 1670d86545..693c66ccb0 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -232,7 +232,7 @@ namespace osu.Game.Tests.Visual.Background AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" }, - Beatmap = new TestBeatmap(Ruleset.Value).BeatmapInfo, + BeatmapInfo = new TestBeatmap(Ruleset.Value).BeatmapInfo, Ruleset = Ruleset.Value, }))); @@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Background private void setupUserSettings() { AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen()); - AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmap != null); + AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmapInfo != null); AddStep("Set default user settings", () => { SelectedMods.Value = SelectedMods.Value.Concat(new[] { new OsuModNoFail() }).ToArray(); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs new file mode 100644 index 0000000000..2258a209e2 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -0,0 +1,62 @@ +// 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.Input; +using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorSaving : OsuGameTestScene + { + private Editor editor => Game.ChildrenOfType().FirstOrDefault(); + + private EditorBeatmap editorBeatmap => (EditorBeatmap)editor.Dependencies.Get(typeof(EditorBeatmap)); + + /// + /// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select. + /// + [Test] + public void TestNewBeatmapSaveThenLoad() + { + AddStep("set default beatmap", () => Game.Beatmap.SetDefault()); + + PushAndConfirm(() => new EditorLoader()); + + AddUntilStep("wait for editor load", () => editor != null); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + + AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); + AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + + 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); + }); + + AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + + PushAndConfirm(() => new PlaySongSelect()); + + AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault); + AddStep("Open options", () => InputManager.Key(Key.F3)); + AddStep("Enter editor", () => InputManager.Key(Key.Number5)); + + AddUntilStep("Wait for editor load", () => editor != null); + AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index e560c81fb2..7398527f57 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -42,6 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestEmptyLegacyBeatmapSkinFallsBack() { CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null)); + AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value)); } @@ -84,18 +85,18 @@ namespace osu.Game.Tests.Visual.Gameplay Remove(expectedComponentsAdjustmentContainer); return almostEqual(actualInfo, expectedInfo); - - static bool almostEqual(SkinnableInfo info, SkinnableInfo other) => - other != null - && info.Type == other.Type - && info.Anchor == other.Anchor - && info.Origin == other.Origin - && Precision.AlmostEquals(info.Position, other.Position) - && Precision.AlmostEquals(info.Scale, other.Scale) - && Precision.AlmostEquals(info.Rotation, other.Rotation) - && info.Children.SequenceEqual(other.Children, new FuncEqualityComparer(almostEqual)); } + private static bool almostEqual(SkinnableInfo info, SkinnableInfo other) => + other != null + && info.Type == other.Type + && info.Anchor == other.Anchor + && info.Origin == other.Origin + && Precision.AlmostEquals(info.Position, other.Position, 1) + && Precision.AlmostEquals(info.Scale, other.Scale) + && Precision.AlmostEquals(info.Rotation, other.Rotation) + && info.Children.SequenceEqual(other.Children, new FuncEqualityComparer(almostEqual)); + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs new file mode 100644 index 0000000000..4c48d52acd --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs @@ -0,0 +1,108 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestScenePerformancePointsCounter : OsuTestScene + { + [Cached] + private GameplayState gameplayState; + + [Cached] + private ScoreProcessor scoreProcessor; + + private int iteration; + private PerformancePointsCounter counter; + + public TestScenePerformancePointsCounter() + { + var ruleset = CreateRuleset(); + + Debug.Assert(ruleset != null); + + var beatmap = CreateWorkingBeatmap(ruleset.RulesetInfo) + .GetPlayableBeatmap(ruleset.RulesetInfo); + + gameplayState = new GameplayState(beatmap, ruleset); + scoreProcessor = new ScoreProcessor(); + } + + protected override Ruleset CreateRuleset() => new OsuRuleset(); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create counter", () => + { + iteration = 0; + + Child = counter = new PerformancePointsCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(5), + }; + }); + } + + [Test] + public void TestBasicCounting() + { + int previousValue = 0; + + AddAssert("counter displaying zero", () => counter.Current.Value == 0); + + AddRepeatStep("Add judgement", applyOneJudgement, 10); + + AddUntilStep("counter non-zero", () => counter.Current.Value > 0); + AddUntilStep("counter opaque", () => counter.Child.Alpha == 1); + + AddStep("Revert judgement", () => + { + previousValue = counter.Current.Value; + + scoreProcessor.RevertResult(new JudgementResult(new HitObject(), new OsuJudgement())); + }); + + AddUntilStep("counter decreased", () => counter.Current.Value < previousValue); + + AddStep("Add judgement", applyOneJudgement); + + AddUntilStep("counter non-zero", () => counter.Current.Value > 0); + } + + private void applyOneJudgement() + { + var scoreInfo = gameplayState.Score.ScoreInfo; + + scoreInfo.MaxCombo = iteration * 1000; + scoreInfo.Accuracy = 1; + scoreInfo.Statistics[HitResult.Great] = iteration * 1000; + + scoreProcessor.ApplyResult(new OsuJudgementResult(new HitObject + { + StartTime = iteration * 10000, + }, new OsuJudgement()) + { + Type = HitResult.Perfect, + }); + + iteration++; + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 8160a62991..aee15a145c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.Gameplay 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 @@ namespace osu.Game.Tests.Visual.Gameplay }); 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/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 1809332bce..5e2374cbcb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online; @@ -8,34 +9,29 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Scoring; using osu.Game.Users; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Screens.Ranking; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public class TestSceneReplayDownloadButton : OsuTestScene + public class TestSceneReplayDownloadButton : OsuManualInputManagerTestScene { [Resolved] private RulesetStore rulesets { get; set; } private TestReplayDownloadButton downloadButton; - public TestSceneReplayDownloadButton() + [Test] + public void TestDisplayStates() { - createButton(true); - AddStep(@"downloading state", () => downloadButton.SetDownloadState(DownloadState.Downloading)); - AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable)); - AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded)); - createButton(false); - createButtonNoScore(); - } - - private void createButton(bool withReplay) - { - AddStep(withReplay ? @"create button with replay" : "create button without replay", () => + AddStep(@"create button with replay", () => { - Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(withReplay)) + Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(true)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -43,9 +39,81 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddStep(@"downloading state", () => downloadButton.SetDownloadState(DownloadState.Downloading)); + AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable)); + AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded)); } - private void createButtonNoScore() + [Test] + public void TestButtonWithReplayStartsDownload() + { + bool downloadStarted = false; + bool downloadFinished = false; + + AddStep(@"create button with replay", () => + { + downloadStarted = false; + downloadFinished = false; + + Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(true)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + + downloadButton.State.BindValueChanged(state => + { + switch (state.NewValue) + { + case DownloadState.Downloading: + downloadStarted = true; + break; + } + + switch (state.OldValue) + { + case DownloadState.Downloading: + downloadFinished = true; + break; + } + }); + }); + + AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddAssert("state is available", () => downloadButton.State.Value == DownloadState.NotDownloaded); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(downloadButton); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("state entered downloading", () => downloadStarted); + AddUntilStep("state left downloading", () => downloadFinished); + } + + [Test] + public void TestButtonWithoutReplay() + { + AddStep("create button without replay", () => + { + Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(false)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); + + AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddAssert("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded); + AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value); + } + + [Test] + public void CreateButtonWithNoScore() { AddStep("create button with null score", () => { @@ -57,6 +125,9 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddAssert("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded); + AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value); } private ScoreInfo getScoreInfo(bool replayAvailable) @@ -78,6 +149,8 @@ namespace osu.Game.Tests.Visual.Gameplay { public void SetDownloadState(DownloadState state) => State.Value = state; + public new Bindable State => base.State; + public TestReplayDownloadButton(ScoreInfo score) : base(score) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 0a3fedaf8e..c8040f42f0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -17,6 +18,8 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -38,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay private TestReplayRecorder recorder; [Cached] - private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); [SetUp] public void SetUp() => Schedule(() => @@ -57,7 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay Recorder = recorder = new TestReplayRecorder(new Score { Replay = replay, - ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo } + ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo } }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index dfd5e2dc58..3545fc96e8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -13,6 +14,8 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -30,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly TestRulesetInputManager recordingManager; [Cached] - private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); public TestSceneReplayRecording() { @@ -48,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay Recorder = new TestReplayRecorder(new Score { Replay = replay, - ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo } + ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo } }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 6f5f774758..b4de060578 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -25,6 +25,8 @@ using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.UI; @@ -62,7 +64,7 @@ namespace osu.Game.Tests.Visual.Gameplay private SpectatorClient spectatorClient { get; set; } [Cached] - private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); [SetUp] public void SetUp() => Schedule(() => @@ -354,7 +356,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { public TestReplayRecorder() - : base(new Score { ScoreInfo = { Beatmap = new BeatmapInfo() } }) + : base(new Score { ScoreInfo = { BeatmapInfo = new BeatmapInfo() } }) { } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs index 5fdadfc2fb..4754a73f83 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays.Login; namespace osu.Game.Tests.Visual.Menus @@ -30,12 +31,25 @@ namespace osu.Game.Tests.Visual.Menus } [Test] - public void TestBasicLogin() + public void TestLoginSuccess() { AddStep("logout", () => API.Logout()); AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); } + + [Test] + public void TestLoginFailure() + { + AddStep("logout", () => + { + API.Logout(); + ((DummyAPIAccess)API).FailNextLogin(); + }); + + AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + } } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 9037338e23..79dfe79299 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("import beatmap with track", () => { var setWithTrack = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).Result; - Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Beatmaps.First()); + Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Value.Beatmaps.First()); }); AddStep("bind to track change", () => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index b1f5781f6f..22ff2b98ce 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -43,11 +43,12 @@ namespace osu.Game.Tests.Visual.Multiplayer Spacing = new Vector2(10), Children = new Drawable[] { - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Flyte's Trash Playlist" }, + Name = { Value = "Multiplayer room" }, Status = { Value = new RoomStatusOpen() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, + Type = { Value = MatchType.HeadToHead }, Playlist = { new PlaylistItem @@ -65,9 +66,9 @@ namespace osu.Game.Tests.Visual.Multiplayer } } }), - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Room 2" }, + Name = { Value = "Playlist room with multiple beatmaps" }, Status = { Value = new RoomStatusPlaying() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, Playlist = @@ -100,15 +101,15 @@ namespace osu.Game.Tests.Visual.Multiplayer } } }), - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Room 3" }, + Name = { Value = "Finished room" }, Status = { Value = new RoomStatusEnded() }, EndDate = { Value = DateTimeOffset.Now }, }), - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Room 4 (spotlight)" }, + Name = { Value = "Spotlight room" }, Status = { Value = new RoomStatusOpen() }, Category = { Value = RoomCategory.Spotlight }, }), @@ -123,14 +124,14 @@ namespace osu.Game.Tests.Visual.Multiplayer DrawableRoom drawableRoom = null; Room room = null; - AddStep("create room", () => Child = drawableRoom = createDrawableRoom(room = new Room + AddStep("create room", () => Child = drawableRoom = createLoungeRoom(room = new Room { Name = { Value = "Room with password" }, Status = { Value = new RoomStatusOpen() }, Type = { Value = MatchType.HeadToHead }, })); - AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType().Any()); + AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType().Any()); AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); @@ -141,7 +142,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); } - private DrawableRoom createDrawableRoom(Room room) + private DrawableRoom createLoungeRoom(Room room) { room.Host.Value ??= new User { Username = "peppy", Id = 2 }; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs similarity index 66% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs index 50ec2bf3ac..982dfc5cd9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs @@ -13,16 +13,27 @@ using osu.Game.Users.Drawables; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneRecentParticipantsList : OnlinePlayTestScene + public class TestSceneDrawableRoomParticipantsList : OnlinePlayTestScene { - private RecentParticipantsList list; + private DrawableRoomParticipantsList list; [SetUp] public new void Setup() => Schedule(() => { - SelectedRoom.Value = new Room { Name = { Value = "test room" } }; + SelectedRoom.Value = new Room + { + Name = { Value = "test room" }, + Host = + { + Value = new User + { + Id = 2, + Username = "peppy", + } + } + }; - Child = list = new RecentParticipantsList + Child = list = new DrawableRoomParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -40,19 +51,19 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("set 8 circles", () => list.NumberOfCircles = 8); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddStep("add one more user", () => addUser(9)); - AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); + AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); AddStep("remove first user", () => removeUserAt(0)); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddStep("add one more user", () => addUser(9)); - AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); + AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); AddStep("remove last user", () => removeUserAt(8)); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); } [Test] @@ -69,9 +80,9 @@ namespace osu.Game.Tests.Visual.Multiplayer for (int i = 0; i < 8; i++) { AddStep("remove user", () => removeUserAt(0)); - int remainingUsers = 7 - i; + int remainingUsers = 8 - i; - int displayedUsers = remainingUsers > 3 ? 2 : remainingUsers; + int displayedUsers = remainingUsers > 4 ? 3 : remainingUsers; AddAssert($"{displayedUsers} avatars displayed", () => list.ChildrenOfType().Count() == displayedUsers); } } @@ -86,12 +97,12 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("set 3 circles", () => list.NumberOfCircles = 3); - AddAssert("2 users displayed", () => list.ChildrenOfType().Count() == 2); - AddAssert("48 hidden users", () => list.ChildrenOfType().Single().Count == 48); + AddAssert("3 users displayed", () => list.ChildrenOfType().Count() == 3); + AddAssert("48 hidden users", () => list.ChildrenOfType().Single().Count == 48); AddStep("set 10 circles", () => list.NumberOfCircles = 10); - AddAssert("9 users displayed", () => list.ChildrenOfType().Count() == 9); - AddAssert("41 hidden users", () => list.ChildrenOfType().Single().Count == 41); + AddAssert("10 users displayed", () => list.ChildrenOfType().Count() == 10); + AddAssert("41 hidden users", () => list.ChildrenOfType().Single().Count == 41); } [Test] @@ -104,24 +115,24 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("remove from start", () => removeUserAt(0)); - AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); - AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); + AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); + AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); AddStep("remove from end", () => removeUserAt(SelectedRoom.Value.RecentParticipants.Count - 1)); - AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); - AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); + AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); + AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); AddRepeatStep("remove 45 users", () => removeUserAt(0), 45); - AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); - AddAssert("hidden users bubble hidden", () => list.ChildrenOfType().Single().Alpha < 0.5f); + AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("hidden users bubble hidden", () => list.ChildrenOfType().Single().Alpha < 0.5f); AddStep("remove another user", () => removeUserAt(0)); - AddAssert("2 circles displayed", () => list.ChildrenOfType().Count() == 2); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddRepeatStep("remove the remaining two users", () => removeUserAt(0), 2); - AddAssert("0 circles displayed", () => !list.ChildrenOfType().Any()); + AddAssert("1 circle displayed", () => list.ChildrenOfType().Count() == 1); } private void addUser(int id) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs index ff06d4d9c7..5032cdaec7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Accuracy = 0.8, MaxCombo = 500, Combo = 250, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Username = "Test user" }, Date = DateTimeOffset.Now, OnlineScoreID = 12345, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs index 0a8bda7ec0..99d5fd46e9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Accuracy = 0.8, MaxCombo = 500, Combo = 250, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Username = "Test user" }, Date = DateTimeOffset.Now, OnlineScoreID = 12345, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index a8fda19c60..80217a7726 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -68,6 +69,8 @@ namespace osu.Game.Tests.Visual.Multiplayer LoadScreen(dependenciesScreen = new DependenciesScreen(client)); }); + AddUntilStep("wait for dependencies screen", () => Stack.CurrentScreen is DependenciesScreen); + AddUntilStep("wait for dependencies to start load", () => dependenciesScreen.LoadState > LoadState.NotLoaded); AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); AddStep("load multiplayer", () => LoadScreen(multiplayerScreen)); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index e2baa82ba0..7327d4053a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using NUnit.Framework; 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; @@ -123,6 +124,13 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("ruleset unchanged", () => ReferenceEquals(Ruleset.Value, ruleset)); } + [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); }); + } + [Test] public void TestUnavailableRulesetHandled() { diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index f0ddefa51d..5f5ebfccfb 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Navigation Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }, } - }).Result; + }).Result.Value; }); AddAssert($"import {i} succeeded", () => imported != null); diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 52b577b402..aca7ada535 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Navigation Ruleset = new OsuRuleset().RulesetInfo }, } - }).Result; + }).Result.Value; }); } @@ -130,9 +130,9 @@ namespace osu.Game.Tests.Visual.Navigation { Hash = Guid.NewGuid().ToString(), OnlineScoreID = i, - Beatmap = beatmap.Beatmaps.First(), + BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo - }).Result; + }).Result.Value; }); AddAssert($"import {i} succeeded", () => imported != null); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index aeb800f58a..ce437e7299 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -350,13 +350,13 @@ namespace osu.Game.Tests.Visual.Navigation // since most overlays use a scroll container that absorbs on mouse down NowPlayingOverlay nowPlayingOverlay = null; - AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("Wait for now playing load", () => (nowPlayingOverlay = Game.ChildrenOfType().FirstOrDefault()) != null); - AddStep("get and press now playing hotkey", () => - { - nowPlayingOverlay = Game.ChildrenOfType().Single(); - InputManager.Key(Key.F6); - }); + AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("toolbar displayed", () => Game.Toolbar.State.Value == Visibility.Visible); + + AddStep("open now playing", () => InputManager.Key(Key.F6)); + AddUntilStep("now playing is visible", () => nowPlayingOverlay.State.Value == Visibility.Visible); // drag tests @@ -417,7 +417,7 @@ namespace osu.Game.Tests.Visual.Navigation 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/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index f420ad976b..453e26ef96 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -233,7 +233,7 @@ namespace osu.Game.Tests.Visual.Online }); }); - AddAssert("shown beatmaps of current ruleset", () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value))); + AddAssert("shown beatmaps of current ruleset", () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.BeatmapInfo.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value))); AddAssert("left-most beatmap selected", () => overlay.Header.HeaderContent.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs index fd5c188b94..fe8e33f783 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs @@ -58,10 +58,10 @@ namespace osu.Game.Tests.Visual.Online var firstBeatmap = createBeatmap(); var secondBeatmap = createBeatmap(); - AddStep("set first set", () => successRate.Beatmap = firstBeatmap); + AddStep("set first set", () => successRate.BeatmapInfo = firstBeatmap); AddAssert("ratings set", () => successRate.Graph.Metrics == firstBeatmap.Metrics); - AddStep("set second set", () => successRate.Beatmap = secondBeatmap); + AddStep("set second set", () => successRate.BeatmapInfo = secondBeatmap); AddAssert("ratings set", () => successRate.Graph.Metrics == secondBeatmap.Metrics); static BeatmapInfo createBeatmap() => new BeatmapInfo @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOnlyFailMetrics() { - AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo + AddStep("set beatmap", () => successRate.BeatmapInfo = new BeatmapInfo { Metrics = new BeatmapMetrics { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestEmptyMetrics() { - AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo + AddStep("set beatmap", () => successRate.BeatmapInfo = new BeatmapInfo { Metrics = new BeatmapMetrics() }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 609e637914..9562b41363 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -18,6 +19,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Chat; using osu.Game.Overlays; +using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat.Selection; using osu.Game.Overlays.Chat.Tabs; using osu.Game.Users; @@ -41,6 +43,9 @@ namespace osu.Game.Tests.Visual.Online private Channel channel2 => channels[1]; private Channel channel3 => channels[2]; + [CanBeNull] + private Func> onGetMessages; + [Resolved] private GameHost host { get; set; } @@ -79,6 +84,8 @@ namespace osu.Game.Tests.Visual.Online { AddStep("register request handling", () => { + onGetMessages = null; + ((DummyAPIAccess)API).HandleRequest = req => { switch (req) @@ -102,6 +109,12 @@ namespace osu.Game.Tests.Visual.Online } return true; + + case GetMessagesRequest getMessages: + var messages = onGetMessages?.Invoke(getMessages.Channel); + if (messages != null) + getMessages.TriggerSuccess(messages); + return true; } return false; @@ -122,14 +135,37 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestSelectingChannelClosesSelector() + public void TestChannelSelection() { AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); + AddStep("Setup get message response", () => onGetMessages = channel => + { + if (channel == channel1) + { + return new List + { + new Message(1) + { + ChannelId = channel1.Id, + Content = "hello from channel 1!", + Sender = new User + { + Id = 2, + Username = "test_user" + } + } + }; + } + + return null; + }); AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); AddAssert("Current channel is channel 1", () => currentChannel == channel1); + AddUntilStep("Loading spinner hidden", () => chatOverlay.ChildrenOfType().All(spinner => !spinner.IsPresent)); + AddAssert("Channel message shown", () => chatOverlay.ChildrenOfType().Count() == 1); AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs index 5dca218531..513631a221 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Online { PP = 1047.21, Rank = ScoreRank.SH, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Online { PP = 134.32, Rank = ScoreRank.A, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online { PP = 96.83, Rank = ScoreRank.S, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Online var noPPScore = new ScoreInfo { Rank = ScoreRank.B, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 9051c71fc6..d8ec89a94e 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.Playlists { beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1; - importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result; + importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result.Value; }); AddStep("load room", () => diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index a5e2f02f31..df8500fab2 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.Ranking Id = 2, Username = "peppy", }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, TotalScore = 2845370, Accuracy = accuracy, diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 5180854aba..899f351a2a 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) { - Beatmap = createTestBeatmap(author) + BeatmapInfo = createTestBeatmap(author) })); } @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("show excess mods score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo, true) { - Beatmap = createTestBeatmap(author) + BeatmapInfo = createTestBeatmap(author) })); AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Current.Value == "mapper_name")); @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking { AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) { - Beatmap = createTestBeatmap(null) + BeatmapInfo = createTestBeatmap(null) })); AddAssert("mapped by text not present", () => @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(new TestScoreInfo(ruleset.RulesetInfo) { Mods = mods, - Beatmap = beatmap, + BeatmapInfo = beatmap, Date = default, }); }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 631455b727..8d5d0ba8c7 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -337,8 +337,8 @@ namespace osu.Game.Tests.Visual.Ranking public UnrankedSoloResultsScreen(ScoreInfo score) : base(score, true) { - Score.Beatmap.OnlineBeatmapID = 0; - Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending; + Score.BeatmapInfo.OnlineBeatmapID = 0; + Score.BeatmapInfo.Status = BeatmapSetOnlineStatus.Pending; } protected override void LoadComplete() diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index 6f3b3028be..b7b7407428 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -221,6 +221,8 @@ namespace osu.Game.Tests.Visual.Ranking list.SelectedScore.Value = middleScore; }); + AddUntilStep("wait for all scores to be visible", () => list.ChildrenOfType().All(t => t.IsPresent)); + assertScoreState(highestScore, false); assertScoreState(middleScore, true); assertScoreState(lowestScore, false); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 168d9fafcf..1effe52608 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections.Input; using osuTK.Input; @@ -230,6 +231,22 @@ namespace osu.Game.Tests.Visual.Settings AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType().First().IsBinding); } + [Test] + public void TestFilteringHidesResetSectionButtons() + { + SearchTextBox searchTextBox = null; + + AddStep("add any search term", () => + { + searchTextBox = panel.ChildrenOfType().Single(); + searchTextBox.Current.Value = "chat"; + }); + AddUntilStep("all reset section bindings buttons hidden", () => panel.ChildrenOfType().All(button => button.Alpha == 0)); + + AddStep("clear search term", () => searchTextBox.Current.Value = string.Empty); + AddUntilStep("all reset section bindings buttons shown", () => panel.ChildrenOfType().All(button => button.Alpha == 1)); + } + private void checkBinding(string name, string keyName) { AddAssert($"Check {name} is bound to {keyName}", () => diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs index dcc2111ad3..4538e36c5e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestNoMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("no mods selected", () => SelectedMods.Value = Array.Empty()); @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestManiaFirstBarText() { - AddStep("set beatmap", () => advancedStats.Beatmap = new BeatmapInfo + AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo { Ruleset = rulesets.GetRuleset(3), BaseDifficulty = new BeatmapDifficulty @@ -84,11 +84,11 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestEasyMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select EZ mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); SelectedMods.Value = new[] { ruleset.CreateMod() }; }); @@ -101,11 +101,11 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestHardRockMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select HR mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); SelectedMods.Value = new[] { ruleset.CreateMod() }; }); @@ -118,13 +118,13 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestUnchangedDifficultyAdjustMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select unchanged Difficulty Adjust mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); var difficultyAdjustMod = ruleset.CreateMod(); - difficultyAdjustMod.ReadFromDifficulty(advancedStats.Beatmap.BaseDifficulty); + difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.BaseDifficulty); SelectedMods.Value = new[] { difficultyAdjustMod }; }); @@ -137,13 +137,13 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestChangedDifficultyAdjustMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select changed Difficulty Adjust mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); var difficultyAdjustMod = ruleset.CreateMod(); - var originalDifficulty = advancedStats.Beatmap.BaseDifficulty; + var originalDifficulty = advancedStats.BeatmapInfo.BaseDifficulty; difficultyAdjustMod.ReadFromDifficulty(originalDifficulty); difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 78ddfa9ed2..66f15670f5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.SongSelect private readonly Stack selectedSets = new Stack(); private readonly HashSet eagerSelectedIDs = new HashSet(); - private BeatmapInfo currentSelection => carousel.SelectedBeatmap; + private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo; private const int set_count = 5; @@ -75,11 +75,11 @@ namespace osu.Game.Tests.Visual.SongSelect { for (int i = 0; i < 3; i++) { - AddStep("store selection", () => selection = carousel.SelectedBeatmap); + AddStep("store selection", () => selection = carousel.SelectedBeatmapInfo); if (isIterating) - AddUntilStep("selection changed", () => carousel.SelectedBeatmap != selection); + AddUntilStep("selection changed", () => carousel.SelectedBeatmapInfo != selection); else - AddUntilStep("selection not changed", () => carousel.SelectedBeatmap == selection); + AddUntilStep("selection not changed", () => carousel.SelectedBeatmapInfo == selection); } } } @@ -387,7 +387,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Set non-empty mode filter", () => carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }, false)); - AddAssert("Something is selected", () => carousel.SelectedBeatmap != null); + AddAssert("Something is selected", () => carousel.SelectedBeatmapInfo != null); } /// @@ -562,7 +562,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false)); AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false)); - AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap.RulesetID == 0); + AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmapInfo.RulesetID == 0); AddStep("remove mixed set", () => { @@ -653,7 +653,7 @@ namespace osu.Game.Tests.Visual.SongSelect carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); }); - AddAssert("selection lost", () => carousel.SelectedBeatmap == null); + AddAssert("selection lost", () => carousel.SelectedBeatmapInfo == null); AddStep("Restore different ruleset filter", () => { @@ -661,7 +661,7 @@ namespace osu.Game.Tests.Visual.SongSelect eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); }); - AddAssert("selection changed", () => carousel.SelectedBeatmap != manySets.First().Beatmaps.First()); + AddAssert("selection changed", () => carousel.SelectedBeatmapInfo != manySets.First().Beatmaps.First()); } AddAssert("Selection was random", () => eagerSelectedIDs.Count > 2); @@ -763,9 +763,9 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => { if (diff != null) - return carousel.SelectedBeatmap == carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Skip(diff.Value - 1).First(); + return carousel.SelectedBeatmapInfo == carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Skip(diff.Value - 1).First(); - return carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Contains(carousel.SelectedBeatmap); + return carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Contains(carousel.SelectedBeatmapInfo); }); private void setSelected(int set, int diff) => @@ -800,7 +800,7 @@ namespace osu.Game.Tests.Visual.SongSelect { carousel.RandomAlgorithm.Value = RandomSelectAlgorithm.RandomPermutation; - if (!selectedSets.Any() && carousel.SelectedBeatmap != null) + if (!selectedSets.Any() && carousel.SelectedBeatmapInfo != null) selectedSets.Push(carousel.SelectedBeatmapSet); carousel.SelectNextRandom(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs index b4544fbc85..d5b4fb9a80 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestAllMetrics() { - AddStep("all metrics", () => details.Beatmap = new BeatmapInfo + AddStep("all metrics", () => details.BeatmapInfo = new BeatmapInfo { BeatmapSet = new BeatmapSetInfo { @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestAllMetricsExceptSource() { - AddStep("all except source", () => details.Beatmap = new BeatmapInfo + AddStep("all except source", () => details.BeatmapInfo = new BeatmapInfo { BeatmapSet = new BeatmapSetInfo { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOnlyRatings() { - AddStep("ratings", () => details.Beatmap = new BeatmapInfo + AddStep("ratings", () => details.BeatmapInfo = new BeatmapInfo { BeatmapSet = new BeatmapSetInfo { @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOnlyFailsAndRetries() { - AddStep("fails retries", () => details.Beatmap = new BeatmapInfo + AddStep("fails retries", () => details.BeatmapInfo = new BeatmapInfo { Version = "Only Retries and Fails", Metadata = new BeatmapMetadata @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestNoMetrics() { - AddStep("no metrics", () => details.Beatmap = new BeatmapInfo + AddStep("no metrics", () => details.BeatmapInfo = new BeatmapInfo { Version = "No Metrics", Metadata = new BeatmapMetadata @@ -166,13 +166,13 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestNullBeatmap() { - AddStep("null beatmap", () => details.Beatmap = null); + AddStep("null beatmap", () => details.BeatmapInfo = null); } [Test] public void TestOnlineMetrics() { - AddStep("online ratings/retries/fails", () => details.Beatmap = new BeatmapInfo + AddStep("online ratings/retries/fails", () => details.BeatmapInfo = new BeatmapInfo { OnlineBeatmapID = 162, }); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 29815ce9ff..13b769c80a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.SongSelect beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); - leaderboard.Beatmap = beatmapInfo; + leaderboard.BeatmapInfo = beatmapInfo; }); clearScores(); @@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void checkCount(int expected) => AddUntilStep("Correct count displayed", () => leaderboard.ChildrenOfType().Count() == expected); - private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmap) + private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmapInfo) { return new[] { @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + BeatmapInfo = beatmapInfo, User = new User { Id = 6602580, @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + BeatmapInfo = beatmapInfo, User = new User { Id = 4608074, @@ -235,7 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + BeatmapInfo = beatmapInfo, User = new User { Id = 1014222, @@ -254,7 +254,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + BeatmapInfo = beatmapInfo, User = new User { Id = 1541390, @@ -273,7 +273,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + BeatmapInfo = beatmapInfo, User = new User { Id = 2243452, @@ -292,7 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + BeatmapInfo = beatmapInfo, User = new User { Id = 2705430, @@ -311,7 +311,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + BeatmapInfo = beatmapInfo, User = new User { Id = 7151382, @@ -330,7 +330,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + BeatmapInfo = beatmapInfo, User = new User { Id = 2051389, @@ -349,7 +349,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + BeatmapInfo = beatmapInfo, User = new User { Id = 6169483, @@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + BeatmapInfo = beatmapInfo, User = new User { Id = 6702666, @@ -385,7 +385,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void showBeatmapWithStatus(BeatmapSetOnlineStatus status) { - leaderboard.Beatmap = new BeatmapInfo + leaderboard.BeatmapInfo = new BeatmapInfo { OnlineBeatmapID = 1113057, Status = status, diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 53cb628bb3..c22b6a54e9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -192,7 +192,7 @@ namespace osu.Game.Tests.Visual.SongSelect }).ToList() }; - return Game.BeatmapManager.Import(beatmapSet).Result; + return Game.BeatmapManager.Import(beatmapSet).Result.Value; } private bool ensureAllBeatmapSetsImported(IEnumerable beatmapSets) => beatmapSets.All(set => set != null); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 102e5ee425..067f1cabb4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select next and enter", () => { InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType() - .First(b => ((CarouselBeatmap)b.Item).Beatmap != songSelect.Carousel.SelectedBeatmap)); + .First(b => ((CarouselBeatmap)b.Item).BeatmapInfo != songSelect.Carousel.SelectedBeatmapInfo)); InputManager.Click(MouseButton.Left); @@ -172,7 +172,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select next and enter", () => { InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType() - .First(b => ((CarouselBeatmap)b.Item).Beatmap != songSelect.Carousel.SelectedBeatmap)); + .First(b => ((CarouselBeatmap)b.Item).BeatmapInfo != songSelect.Carousel.SelectedBeatmapInfo)); InputManager.PressButton(MouseButton.Left); @@ -312,7 +312,7 @@ namespace osu.Game.Tests.Visual.SongSelect { createSongSelect(); addRulesetImportStep(2); - AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); } [Test] @@ -322,13 +322,13 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); addRulesetImportStep(1); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); changeRuleset(1); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 1); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 1); changeRuleset(0); - AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); } [Test] @@ -338,7 +338,7 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); addRulesetImportStep(0); addRulesetImportStep(0); @@ -355,7 +355,7 @@ namespace osu.Game.Tests.Visual.SongSelect Beatmap.Value = manager.GetWorkingBeatmap(target); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(target)); // this is an important check, to make sure updateComponentFromBeatmap() was actually run AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo.Equals(target)); @@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); addRulesetImportStep(0); addRulesetImportStep(0); @@ -385,7 +385,7 @@ namespace osu.Game.Tests.Visual.SongSelect Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(target)); AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0); @@ -444,7 +444,7 @@ namespace osu.Game.Tests.Visual.SongSelect { createSongSelect(); addManyTestMaps(); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); bool startRequested = false; @@ -473,13 +473,13 @@ namespace osu.Game.Tests.Visual.SongSelect // used for filter check below AddStep("allow convert display", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nonono"); AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); - AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); BeatmapInfo target = null; @@ -494,7 +494,7 @@ namespace osu.Game.Tests.Visual.SongSelect Beatmap.Value = manager.GetWorkingBeatmap(target); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddAssert("selected only shows expected ruleset (plus converts)", () => { @@ -502,16 +502,16 @@ namespace osu.Game.Tests.Visual.SongSelect // special case for converts checked here. return selectedPanel.ChildrenOfType().All(i => - i.IsFiltered || i.Item.Beatmap.Ruleset.ID == targetRuleset || i.Item.Beatmap.Ruleset.ID == 0); + i.IsFiltered || i.Item.BeatmapInfo.Ruleset.ID == targetRuleset || i.Item.BeatmapInfo.Ruleset.ID == 0); }); - AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.OnlineBeatmapID == target.OnlineBeatmapID); + AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.OnlineBeatmapID == target.OnlineBeatmapID); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); AddStep("reset filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = string.Empty); AddAssert("game still correct", () => Beatmap.Value?.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); - AddAssert("carousel still correct", () => songSelect.Carousel.SelectedBeatmap.OnlineBeatmapID == target.OnlineBeatmapID); + AddAssert("carousel still correct", () => songSelect.Carousel.SelectedBeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); } [Test] @@ -522,13 +522,13 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(0); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nonono"); AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); - AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); BeatmapInfo target = null; @@ -540,15 +540,15 @@ namespace osu.Game.Tests.Visual.SongSelect Beatmap.Value = manager.GetWorkingBeatmap(target); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); - AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.OnlineBeatmapID == target.OnlineBeatmapID); + AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.OnlineBeatmapID == target.OnlineBeatmapID); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nononoo"); AddUntilStep("game lost selection", () => Beatmap.Value is DummyWorkingBeatmap); - AddAssert("carousel lost selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddAssert("carousel lost selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); } [Test] @@ -581,9 +581,9 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); addRulesetImportStep(0); AddStep("Move to last difficulty", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.Last())); - AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmap.ID); + AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmapInfo.ID); AddStep("Hide first beatmap", () => manager.Hide(songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First())); - AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmap.ID == previousID); + AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmapInfo.ID == previousID); } [Test] @@ -641,7 +641,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); + AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmapInfo == filteredBeatmap); } [Test] @@ -714,10 +714,11 @@ namespace osu.Game.Tests.Visual.SongSelect }); FilterableDifficultyIcon difficultyIcon = null; - AddStep("Find an icon for different ruleset", () => + AddUntilStep("Find an icon for different ruleset", () => { difficultyIcon = set.ChildrenOfType() - .First(icon => icon.Item.Beatmap.Ruleset.ID == 3); + .FirstOrDefault(icon => icon.Item.BeatmapInfo.Ruleset.ID == 3); + return difficultyIcon != null; }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); @@ -735,7 +736,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); - AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmap.BeatmapSet.ID == previousSetID); + AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmapInfo.BeatmapSet.ID == previousSetID); AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.ID == 3); } @@ -751,7 +752,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import huge difficulty count map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); - imported = manager.Import(createTestBeatmapSet(usableRulesets, 50)).Result; + imported = manager.Import(createTestBeatmapSet(usableRulesets, 50)).Result.Value; }); AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported.Beatmaps.First())); @@ -767,7 +768,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Find group icon for different ruleset", () => { groupIcon = set.ChildrenOfType() - .First(icon => icon.Items.First().Beatmap.Ruleset.ID == 3); + .First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3); }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); @@ -781,7 +782,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); - AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo.Equals(groupIcon.Items.First().Beatmap)); + AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo.Equals(groupIcon.Items.First().BeatmapInfo)); } [Test] @@ -805,7 +806,7 @@ namespace osu.Game.Tests.Visual.SongSelect songSelect.PresentScore(new ScoreInfo { User = new User { Username = "woo" }, - Beatmap = getPresentBeatmap(), + BeatmapInfo = getPresentBeatmap(), Ruleset = getPresentBeatmap().Ruleset }); }); @@ -837,7 +838,7 @@ namespace osu.Game.Tests.Visual.SongSelect songSelect.PresentScore(new ScoreInfo { User = new User { Username = "woo" }, - Beatmap = getPresentBeatmap(), + BeatmapInfo = getPresentBeatmap(), Ruleset = getPresentBeatmap().Ruleset }); }); @@ -856,7 +857,7 @@ namespace osu.Game.Tests.Visual.SongSelect private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); - private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap); + private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmapInfo); private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon) { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 2e30ed9827..189b143a35 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -37,7 +37,8 @@ namespace osu.Game.Tests.Visual.UserInterface private ScoreManager scoreManager; private readonly List importedScores = new List(); - private BeatmapInfo beatmap; + + private BeatmapInfo beatmapInfo; [Cached] private readonly DialogOverlay dialogOverlay; @@ -55,7 +56,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Size = new Vector2(550f, 450f), Scope = BeatmapLeaderboardScope.Local, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { ID = 1, Metadata = new BeatmapMetadata @@ -84,15 +85,15 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler)); - beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0]; + beatmapInfo = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Value.Beatmaps[0]; for (int i = 0; i < 50; i++) { var score = new ScoreInfo { OnlineScoreID = i, - Beatmap = beatmap, - BeatmapInfoID = beatmap.ID, + BeatmapInfo = beatmapInfo, + BeatmapInfoID = beatmapInfo.ID, Accuracy = RNG.NextDouble(), TotalScore = RNG.Next(1, 1000000), MaxCombo = RNG.Next(1, 1000), @@ -100,7 +101,7 @@ namespace osu.Game.Tests.Visual.UserInterface User = new User { Username = "TestUser" }, }; - importedScores.Add(scoreManager.Import(score).Result); + importedScores.Add(scoreManager.Import(score).Result.Value); } return dependencies; @@ -115,7 +116,7 @@ namespace osu.Game.Tests.Visual.UserInterface leaderboard.Scores = null; leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables - leaderboard.Beatmap = beatmap; + leaderboard.BeatmapInfo = beatmapInfo; leaderboard.RefreshScores(); // Required in the case that the beatmap hasn't changed }); @@ -161,6 +162,8 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); + AddUntilStep("wait for fetch", () => leaderboard.Scores != null); + AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID)); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs new file mode 100644 index 0000000000..eedafce271 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs @@ -0,0 +1,77 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneOsuFont : OsuTestScene + { + private OsuSpriteText spriteText; + + private readonly BindableBool useAlternates = new BindableBool(); + private readonly Bindable weight = new Bindable(FontWeight.Regular); + + [BackgroundDependencyLoader] + private void load() + { + Child = spriteText = new OsuSpriteText + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AllowMultiline = true, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + useAlternates.BindValueChanged(_ => updateFont()); + weight.BindValueChanged(_ => updateFont(), true); + } + + private void updateFont() + { + FontUsage usage = useAlternates.Value ? OsuFont.TorusAlternate : OsuFont.Torus; + spriteText.Font = usage.With(size: 40, weight: weight.Value); + } + + [Test] + public void TestTorusAlternates() + { + AddStep("set all ASCII letters", () => spriteText.Text = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ +abcdefghijklmnopqrstuvwxyz"); + AddStep("set all alternates", () => spriteText.Text = @"A Á Ă Â Ä À Ā Ą Å Ã +Æ B D Ð Ď Đ E É Ě Ê +Ë Ė È Ē Ę F G Ğ Ģ Ġ +H I Í Î Ï İ Ì Ī Į K +Ķ O Œ P Þ Q R Ŕ Ř Ŗ +T Ŧ Ť Ţ Ț V W Ẃ Ŵ Ẅ +Ẁ X Y Ý Ŷ Ÿ Ỳ a á ă +â ä à ā ą å ã æ b d +ď đ e é ě ê ë ė è ē +ę f g ğ ģ ġ k ķ m n +ń ň ņ ŋ ñ o œ p þ q +t ŧ ť ţ ț u ú û ü ù +ű ū ų ů w ẃ ŵ ẅ ẁ x +y ý ŷ ÿ ỳ"); + + AddToggleStep("toggle alternates", alternates => useAlternates.Value = alternates); + + addSetWeightStep(FontWeight.Light); + addSetWeightStep(FontWeight.Regular); + addSetWeightStep(FontWeight.SemiBold); + addSetWeightStep(FontWeight.Bold); + + void addSetWeightStep(FontWeight newWeight) => AddStep($"set weight {newWeight}", () => weight.Value = newWeight); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs index 198cc70e01..74cd675a05 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.UserInterface var req = new GetBeatmapSetRequest(1); api.Queue(req); - AddUntilStep("wait for api response", () => req.Result != null); + AddUntilStep("wait for api response", () => req.Response != null); TestUpdateableBeatmapBackgroundSprite background = null; @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.UserInterface Child = background = new TestUpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both, - Beatmap = { Value = new BeatmapInfo { BeatmapSet = req.Result?.ToBeatmapSet(rulesets) } } + Beatmap = { Value = new BeatmapInfo { BeatmapSet = req.Response?.ToBeatmapSet(rulesets) } } }; }); diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 696f930467..cd56cb51ae 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -4,6 +4,7 @@ + diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs index bc32a12ab7..f9c553cb3f 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Tests.Components private void success(APIBeatmap apiBeatmap) { - var beatmap = apiBeatmap.ToBeatmap(rulesets); + var beatmap = apiBeatmap.ToBeatmapInfo(rulesets); Add(new TournamentBeatmapPanel(beatmap) { Anchor = Anchor.Centre, diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs index 47e7ed9b61..27eb55a9fb 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Tests.Components private FillFlowContainer fillFlow; - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; [BackgroundDependencyLoader] private void load() @@ -44,12 +44,12 @@ namespace osu.Game.Tournament.Tests.Components private void success(APIBeatmap apiBeatmap) { - beatmap = apiBeatmap.ToBeatmap(rulesets); + beatmapInfo = apiBeatmap.ToBeatmapInfo(rulesets); var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().AllMods; foreach (var mod in mods) { - fillFlow.Add(new TournamentBeatmapPanel(beatmap, mod.Acronym) + fillFlow.Add(new TournamentBeatmapPanel(beatmapInfo, mod.Acronym) { Anchor = Anchor.Centre, Origin = Anchor.Centre diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 6080f7b636..13888699ef 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -21,22 +21,22 @@ namespace osu.Game.Tournament.Components { public class SongBar : CompositeDrawable { - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; public const float HEIGHT = 145 / 2f; [Resolved] private IBindable ruleset { get; set; } - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (beatmap == value) + if (beatmapInfo == value) return; - beatmap = value; + beatmapInfo = value; update(); } } @@ -95,18 +95,18 @@ namespace osu.Game.Tournament.Components private void update() { - if (beatmap == null) + if (beatmapInfo == null) { flow.Clear(); return; } - var bpm = beatmap.BeatmapSet.OnlineInfo.BPM; - var length = beatmap.Length; + var bpm = beatmapInfo.BeatmapSet.OnlineInfo.BPM; + var length = beatmapInfo.Length; string hardRockExtra = ""; string srExtra = ""; - var ar = beatmap.BaseDifficulty.ApproachRate; + var ar = beatmapInfo.BaseDifficulty.ApproachRate; if ((mods & LegacyMods.HardRock) > 0) { @@ -117,7 +117,7 @@ namespace osu.Game.Tournament.Components if ((mods & LegacyMods.DoubleTime) > 0) { // temporary local calculation (taken from OsuDifficultyCalculator) - double preempt = (int)BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450) / 1.5; + double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(ar, 1800, 1200, 450) / 1.5; ar = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5); bpm *= 1.5f; @@ -132,9 +132,9 @@ namespace osu.Game.Tournament.Components default: stats = new (string heading, string content)[] { - ("CS", $"{beatmap.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), + ("CS", $"{beatmapInfo.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), ("AR", $"{ar:0.#}{hardRockExtra}"), - ("OD", $"{beatmap.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), + ("OD", $"{beatmapInfo.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), }; break; @@ -142,15 +142,15 @@ namespace osu.Game.Tournament.Components case 3: stats = new (string heading, string content)[] { - ("OD", $"{beatmap.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), - ("HP", $"{beatmap.BaseDifficulty.DrainRate:0.#}{hardRockExtra}") + ("OD", $"{beatmapInfo.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), + ("HP", $"{beatmapInfo.BaseDifficulty.DrainRate:0.#}{hardRockExtra}") }; break; case 2: stats = new (string heading, string content)[] { - ("CS", $"{beatmap.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), + ("CS", $"{beatmapInfo.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), ("AR", $"{ar:0.#}"), }; break; @@ -186,7 +186,7 @@ namespace osu.Game.Tournament.Components Children = new Drawable[] { new DiffPiece(stats), - new DiffPiece(("Star Rating", $"{beatmap.StarDifficulty:0.#}{srExtra}")) + new DiffPiece(("Star Rating", $"{beatmapInfo.StarDifficulty:0.#}{srExtra}")) } }, new FillFlowContainer @@ -229,7 +229,7 @@ namespace osu.Game.Tournament.Components } } }, - new TournamentBeatmapPanel(beatmap) + new TournamentBeatmapPanel(beatmapInfo) { RelativeSizeAxes = Axes.X, Width = 0.5f, diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index e6d73c6e83..0e5a66e7fe 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Components { public class TournamentBeatmapPanel : CompositeDrawable { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; private readonly string mod; private const float horizontal_padding = 10; @@ -32,11 +32,11 @@ namespace osu.Game.Tournament.Components private readonly Bindable currentMatch = new Bindable(); private Box flash; - public TournamentBeatmapPanel(BeatmapInfo beatmap, string mod = null) + public TournamentBeatmapPanel(BeatmapInfo beatmapInfo, string mod = null) { - if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); + if (beatmapInfo == null) throw new ArgumentNullException(nameof(beatmapInfo)); - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; this.mod = mod; Width = 400; Height = HEIGHT; @@ -61,7 +61,7 @@ namespace osu.Game.Tournament.Components { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.5f), - BeatmapSet = Beatmap.BeatmapSet, + BeatmapSet = BeatmapInfo.BeatmapSet, }, new FillFlowContainer { @@ -75,8 +75,8 @@ namespace osu.Game.Tournament.Components new TournamentSpriteText { Text = new RomanisableString( - $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", - $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}"), + $"{BeatmapInfo.Metadata.ArtistUnicode ?? BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.TitleUnicode ?? BeatmapInfo.Metadata.Title}", + $"{BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.Title}"), Font = OsuFont.Torus.With(weight: FontWeight.Bold), }, new FillFlowContainer @@ -93,7 +93,7 @@ namespace osu.Game.Tournament.Components }, new TournamentSpriteText { - Text = Beatmap.Metadata.AuthorString, + Text = BeatmapInfo.Metadata.AuthorString, Padding = new MarginPadding { Right = 20 }, Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, @@ -105,7 +105,7 @@ namespace osu.Game.Tournament.Components }, new TournamentSpriteText { - Text = Beatmap.Version, + Text = BeatmapInfo.Version, Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, } @@ -149,7 +149,7 @@ namespace osu.Game.Tournament.Components private void updateState() { - var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == Beatmap.OnlineBeatmapID); + var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineBeatmapID); bool doFlash = found != choice; choice = found; diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index f538d4a7d9..7010a30eb7 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tournament.IPC else { beatmapLookupRequest = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId }); - beatmapLookupRequest.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets); + beatmapLookupRequest.Success += b => Beatmap.Value = b.ToBeatmapInfo(Rulesets); API.Queue(beatmapLookupRequest); } } diff --git a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs index 50498304ca..b94b164116 100644 --- a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs +++ b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tournament.Screens private void beatmapChanged(ValueChangedEvent beatmap) { SongBar.FadeInFromZero(300, Easing.OutQuint); - SongBar.Beatmap = beatmap.NewValue; + SongBar.BeatmapInfo = beatmap.NewValue; } } } diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 27ad6650d1..6e4fc8fe1a 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -238,7 +238,7 @@ namespace osu.Game.Tournament.Screens.Editors req.Success += res => { - Model.BeatmapInfo = res.ToBeatmap(rulesets); + Model.BeatmapInfo = res.ToBeatmapInfo(rulesets); updatePanel(); }; diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 6418bf97da..b64a3993e6 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -246,7 +246,7 @@ namespace osu.Game.Tournament.Screens.Editors req.Success += res => { - Model.BeatmapInfo = res.ToBeatmap(rulesets); + Model.BeatmapInfo = res.ToBeatmapInfo(rulesets); updatePanel(); }; diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index d4292c5492..1e3c550323 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -147,11 +147,11 @@ namespace osu.Game.Tournament.Screens.MapPool if (map != null) { - if (e.Button == MouseButton.Left && map.Beatmap.OnlineBeatmapID != null) - addForBeatmap(map.Beatmap.OnlineBeatmapID.Value); + if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineBeatmapID != null) + addForBeatmap(map.BeatmapInfo.OnlineBeatmapID.Value); else { - var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.Beatmap.OnlineBeatmapID); + var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineBeatmapID); if (existing != null) { diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 531da00faf..bdf7269c83 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -182,7 +182,7 @@ namespace osu.Game.Tournament { var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); API.Perform(req); - b.BeatmapInfo = req.Result?.ToBeatmap(RulesetStore); + b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore); addedInfo = true; } @@ -203,7 +203,7 @@ namespace osu.Game.Tournament { var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); req.Perform(API); - b.BeatmapInfo = req.Result?.ToBeatmap(RulesetStore); + b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore); addedInfo = true; } diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 1844b193f2..2bb0787b4c 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -5,7 +5,7 @@ using osu.Game.Database; namespace osu.Game.Beatmaps { - public class BeatmapDifficulty : IHasPrimaryKey + public class BeatmapDifficulty : IHasPrimaryKey, IBeatmapDifficultyInfo { /// /// The default value used for all difficulty settings except and . @@ -20,6 +20,15 @@ namespace osu.Game.Beatmaps private float? approachRate; + public BeatmapDifficulty() + { + } + + public BeatmapDifficulty(IBeatmapDifficultyInfo source) + { + CopyFrom(source); + } + public float ApproachRate { get => approachRate ?? OverallDifficulty; @@ -39,6 +48,17 @@ namespace osu.Game.Beatmaps return diff; } + public void CopyFrom(IBeatmapDifficultyInfo difficulty) + { + ApproachRate = difficulty.ApproachRate; + DrainRate = difficulty.DrainRate; + CircleSize = difficulty.CircleSize; + OverallDifficulty = difficulty.OverallDifficulty; + + SliderMultiplier = difficulty.SliderMultiplier; + SliderTickRate = difficulty.SliderTickRate; + } + public void CopyTo(BeatmapDifficulty difficulty) { difficulty.ApproachRate = ApproachRate; @@ -49,47 +69,5 @@ namespace osu.Game.Beatmaps difficulty.SliderMultiplier = SliderMultiplier; difficulty.SliderTickRate = SliderTickRate; } - - /// - /// Maps a difficulty value [0, 10] to a two-piece linear range of values. - /// - /// The difficulty value to be mapped. - /// Minimum of the resulting range which will be achieved by a difficulty value of 0. - /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. - /// Maximum of the resulting range which will be achieved by a difficulty value of 10. - /// Value to which the difficulty value maps in the specified range. - public static double DifficultyRange(double difficulty, double min, double mid, double max) - { - if (difficulty > 5) - return mid + (max - mid) * (difficulty - 5) / 5; - if (difficulty < 5) - return mid - (mid - min) * (5 - difficulty) / 5; - - return mid; - } - - /// - /// Maps a difficulty value [0, 10] to a two-piece linear range of values. - /// - /// The difficulty value to be mapped. - /// The values that define the two linear ranges. - /// - /// - /// od0 - /// Minimum of the resulting range which will be achieved by a difficulty value of 0. - /// - /// - /// od5 - /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. - /// - /// - /// od10 - /// Maximum of the resulting range which will be achieved by a difficulty value of 10. - /// - /// - /// - /// Value to which the difficulty value maps in the specified range. - public static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) - => DifficultyRange(difficulty, range.od0, range.od5, range.od10); } } diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 0aa6a6dd0b..3777365088 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -17,6 +17,7 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -147,6 +148,14 @@ namespace osu.Game.Beatmaps }, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } + public Task> GetTimedDifficultyAttributesAsync(WorkingBeatmap beatmap, Ruleset ruleset, Mod[] mods, CancellationToken token = default) + { + return Task.Factory.StartNew(() => ruleset.CreateDifficultyCalculator(beatmap).CalculateTimed(mods), + token, + TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, + updateScheduler); + } + /// /// Retrieves the that describes a star rating. /// @@ -242,7 +251,7 @@ namespace osu.Game.Beatmaps { // GetDifficultyAsync will fall back to existing data from BeatmapInfo if not locally available // (contrary to GetAsync) - GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken) + GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken) .ContinueWith(t => { // We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events. @@ -262,7 +271,7 @@ namespace osu.Game.Beatmaps private StarDifficulty computeDifficulty(in DifficultyCacheLookup key) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. - var beatmapInfo = key.Beatmap; + var beatmapInfo = key.BeatmapInfo; var rulesetInfo = key.Ruleset; try @@ -270,7 +279,7 @@ namespace osu.Game.Beatmaps var ruleset = rulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.Beatmap)); + var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); var attributes = calculator.Calculate(key.OrderedMods); return new StarDifficulty(attributes); @@ -300,21 +309,21 @@ namespace osu.Game.Beatmaps public readonly struct DifficultyCacheLookup : IEquatable { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public readonly RulesetInfo Ruleset; public readonly Mod[] OrderedMods; - public DifficultyCacheLookup([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, IEnumerable mods) + public DifficultyCacheLookup([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo ruleset, IEnumerable mods) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. - Ruleset = ruleset ?? Beatmap.Ruleset; + Ruleset = ruleset ?? BeatmapInfo.Ruleset; OrderedMods = mods?.OrderBy(m => m.Acronym).Select(mod => mod.DeepClone()).ToArray() ?? Array.Empty(); } public bool Equals(DifficultyCacheLookup other) - => Beatmap.ID == other.Beatmap.ID + => BeatmapInfo.ID == other.BeatmapInfo.ID && Ruleset.ID == other.Ruleset.ID && OrderedMods.SequenceEqual(other.OrderedMods); @@ -322,7 +331,7 @@ namespace osu.Game.Beatmaps { var hashCode = new HashCode(); - hashCode.Add(Beatmap.ID); + hashCode.Add(BeatmapInfo.ID); hashCode.Add(Ruleset.ID); foreach (var mod in OrderedMods) @@ -334,12 +343,12 @@ namespace osu.Game.Beatmaps private class BindableStarDifficulty : Bindable { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public readonly CancellationToken CancellationToken; - public BindableStarDifficulty(BeatmapInfo beatmap, CancellationToken cancellationToken) + public BindableStarDifficulty(BeatmapInfo beatmapInfo, CancellationToken cancellationToken) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; CancellationToken = cancellationToken; } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 8cb5da8083..ac5b5d7a8a 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -7,7 +7,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; -using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Rulesets; @@ -17,7 +16,7 @@ namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [Serializable] - public class BeatmapInfo : IEquatable, IHasPrimaryKey + public class BeatmapInfo : IEquatable, IHasPrimaryKey, IBeatmapInfo { public int ID { get; set; } @@ -152,18 +151,7 @@ namespace osu.Game.Beatmaps [JsonIgnore] public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarDifficulty); - public string[] SearchableTerms => new[] - { - Version - }.Concat(Metadata?.SearchableTerms ?? Enumerable.Empty()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); - - public override string ToString() => $"{Metadata ?? BeatmapSet?.Metadata} {versionString}".Trim(); - - public RomanisableString ToRomanisableString() - { - var metadata = (Metadata ?? BeatmapSet?.Metadata)?.ToRomanisableString() ?? new RomanisableString(null, null); - return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); - } + public override string ToString() => this.GetDisplayTitle(); public bool Equals(BeatmapInfo other) { @@ -187,5 +175,22 @@ namespace osu.Game.Beatmaps /// Returns a shallow-clone of this . /// public BeatmapInfo Clone() => (BeatmapInfo)MemberwiseClone(); + + #region Implementation of IHasOnlineID + + public int? OnlineID => OnlineBeatmapID; + + #endregion + + #region Implementation of IBeatmapInfo + + string IBeatmapInfo.DifficultyName => Version; + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; + IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => BaseDifficulty; + IBeatmapSetInfo IBeatmapInfo.BeatmapSet => BeatmapSet; + IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; + double IBeatmapInfo.StarRating => StarDifficulty; + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs new file mode 100644 index 0000000000..eba19ac1a1 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Localisation; + +namespace osu.Game.Beatmaps +{ + public static class BeatmapInfoExtensions + { + /// + /// A user-presentable display title representing this beatmap. + /// + public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{getClosestMetadata(beatmapInfo)} {getVersionString(beatmapInfo)}".Trim(); + + /// + /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. + /// + public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo) + { + var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable(); + var versionString = getVersionString(beatmapInfo); + + return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); + } + + public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[] + { + beatmapInfo.DifficultyName + }.Concat(getClosestMetadata(beatmapInfo).GetSearchableTerms()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); + + private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; + + // temporary helper methods until we figure which metadata should be where. + private static IBeatmapMetadataInfo getClosestMetadata(IBeatmapInfo beatmapInfo) => + beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet?.Metadata ?? new BeatmapMetadata(); + } +} diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index bd85017d58..240db22c00 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -6,111 +6,68 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; -using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; -using osu.Framework.Lists; -using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Statistics; using osu.Framework.Testing; -using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; -using osu.Game.Rulesets.Objects; using osu.Game.Skinning; using osu.Game.Users; -using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps { /// - /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. + /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable, IBeatmapResourceProvider + public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache, IDisposable { - /// - /// Fired when a single difficulty has been hidden. - /// - public IBindable> BeatmapHidden => beatmapHidden; + private readonly BeatmapModelManager beatmapModelManager; + private readonly BeatmapModelDownloader beatmapModelDownloader; - private readonly Bindable> beatmapHidden = new Bindable>(); - - /// - /// Fired when a single difficulty has been restored. - /// - public IBindable> BeatmapRestored => beatmapRestored; - - private readonly Bindable> beatmapRestored = new Bindable>(); - - /// - /// A default representation of a WorkingBeatmap to use when no beatmap is available. - /// - public readonly WorkingBeatmap DefaultBeatmap; - - public override IEnumerable HandledExtensions => new[] { ".osz" }; - - protected override string[] HashableFileTypes => new[] { ".osu" }; - - protected override string ImportFromStablePath => "."; - - protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); - - private readonly RulesetStore rulesets; - private readonly BeatmapStore beatmaps; - private readonly AudioManager audioManager; - private readonly IResourceStore resources; - private readonly LargeTextureStore largeTextureStore; - private readonly ITrackStore trackStore; - - [CanBeNull] - private readonly GameHost host; - - [CanBeNull] - private readonly BeatmapOnlineLookupQueue onlineLookupQueue; + private readonly WorkingBeatmapCache workingBeatmapCache; + private readonly BeatmapOnlineLookupQueue onlineBeatmapLookupQueue; public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) - : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) { - this.rulesets = rulesets; - this.audioManager = audioManager; - this.resources = resources; - this.host = host; + beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host); + beatmapModelDownloader = CreateBeatmapModelDownloader(beatmapModelManager, api, host); + workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host); - DefaultBeatmap = defaultBeatmap; - - beatmaps = (BeatmapStore)ModelStore; - beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference(b); - beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference(b); - beatmaps.ItemRemoved += removeWorkingCache; - beatmaps.ItemUpdated += removeWorkingCache; + workingBeatmapCache.BeatmapManager = beatmapModelManager; + beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache; if (performOnlineLookups) - onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); - - largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); - trackStore = audioManager.GetTrackStore(Files.Store); + { + onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); + beatmapModelManager.OnlineLookupQueue = onlineBeatmapLookupQueue; + } } - protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => - new DownloadBeatmapSetRequest(set, minimiseDownloadSize); + protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host) + { + return new BeatmapModelDownloader(modelManager, api, host); + } - protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; + protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) => + new WorkingBeatmapCache(audioManager, resources, storage, defaultBeatmap, host); + protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) => + new BeatmapModelManager(storage, contextFactory, rulesets, host); + + /// + /// Create a new . + /// public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user) { var metadata = new BeatmapMetadata @@ -134,112 +91,22 @@ namespace osu.Game.Beatmaps } }; - var working = Import(set).Result; - return GetWorkingBeatmap(working.Beatmaps.First()); + var imported = beatmapModelManager.Import(set).Result.Value; + + return GetWorkingBeatmap(imported.Beatmaps.First()); } - protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) - { - if (archive != null) - beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files); - - foreach (BeatmapInfo b in beatmapSet.Beatmaps) - { - // remove metadata from difficulties where it matches the set - if (beatmapSet.Metadata.Equals(b.Metadata)) - b.Metadata = null; - - b.BeatmapSet = beatmapSet; - } - - validateOnlineIds(beatmapSet); - - bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); - - if (onlineLookupQueue != null) - await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); - - // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. - if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) - { - if (beatmapSet.OnlineBeatmapSetID != null) - { - beatmapSet.OnlineBeatmapSetID = null; - LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); - } - } - } - - protected override void PreImport(BeatmapSetInfo beatmapSet) - { - if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null)) - throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}."); - - // check if a set already exists with the same online id, delete if it does. - if (beatmapSet.OnlineBeatmapSetID != null) - { - var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); - - if (existingOnlineId != null) - { - Delete(existingOnlineId); - - // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. - existingOnlineId.OnlineBeatmapSetID = null; - foreach (var b in existingOnlineId.Beatmaps) - b.OnlineBeatmapID = null; - - LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted."); - } - } - } - - private void validateOnlineIds(BeatmapSetInfo beatmapSet) - { - var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); - - // ensure all IDs are unique - if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) - { - LogForModel(beatmapSet, "Found non-unique IDs, resetting..."); - resetIds(); - return; - } - - // find any existing beatmaps in the database that have matching online ids - var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList(); - - if (existingBeatmaps.Count > 0) - { - // reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set. - // we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted. - var existing = CheckForExisting(beatmapSet); - - if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b))) - { - LogForModel(beatmapSet, "Found existing import with IDs already, resetting..."); - resetIds(); - } - } - - void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); - } - - protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) - => base.CheckLocalAvailability(model, items) - || (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID)); + #region Delegation to BeatmapModelManager (methods which previously existed locally). /// - /// Delete a beatmap difficulty. + /// Fired when a single difficulty has been hidden. /// - /// The beatmap difficulty to hide. - public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap); + public IBindable> BeatmapHidden => beatmapModelManager.BeatmapHidden; /// - /// Restore a beatmap difficulty. + /// Fired when a single difficulty has been restored. /// - /// The beatmap difficulty to restore. - public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); + public IBindable> BeatmapRestored => beatmapModelManager.BeatmapRestored; /// /// Saves an file against a given . @@ -247,109 +114,13 @@ namespace osu.Game.Beatmaps /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) - { - var setInfo = info.BeatmapSet; - - using (var stream = new MemoryStream()) - { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); - - stream.Seek(0, SeekOrigin.Begin); - - using (ContextFactory.GetForWrite()) - { - var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); - var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; - - // grab the original file (or create a new one if not found). - var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); - - // metadata may have changed; update the path with the standard format. - beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu"; - beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); - - // update existing or populate new file's filename. - fileInfo.Filename = beatmapInfo.Path; - - stream.Seek(0, SeekOrigin.Begin); - ReplaceFile(setInfo, fileInfo, stream); - } - } - - removeWorkingCache(info); - } - - private readonly WeakList workingCache = new WeakList(); - - /// - /// Retrieve a instance for the provided - /// - /// The beatmap to lookup. - /// A instance correlating to the provided . - public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) - { - // if there are no files, presume the full beatmap info has not yet been fetched from the database. - if (beatmapInfo?.BeatmapSet?.Files.Count == 0) - { - int lookupId = beatmapInfo.ID; - beatmapInfo = QueryBeatmap(b => b.ID == lookupId); - } - - if (beatmapInfo?.BeatmapSet == null) - return DefaultBeatmap; - - lock (workingCache) - { - var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); - if (working != null) - return working; - - beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; - - workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); - - // best effort; may be higher than expected. - GlobalStatistics.Get(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count(); - - return working; - } - } - - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// The first result for the provided query, or null if no results were found. - public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); - - protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import) - { - if (!base.CanSkipImport(existing, import)) - return false; - - return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null); - } - - protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) - { - if (!base.CanReuseExisting(existing, import)) - return false; - - var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); - var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); - - // force re-import if we are not in a sane state. - return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds); - } + public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) => beatmapModelManager.Save(info, beatmapContent, beatmapSkin); /// /// Returns a list of all usable s. /// /// A list of available . - public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => - GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList(); + public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSets(includes, includeProtected); /// /// Returns a list of all usable s. Note that files are not populated. @@ -357,34 +128,7 @@ namespace osu.Game.Beatmaps /// The level of detail to include in the returned objects. /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. /// A list of available . - public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) - { - IQueryable queryable; - - switch (includes) - { - case IncludedDetails.Minimal: - queryable = beatmaps.BeatmapSetsOverview; - break; - - case IncludedDetails.AllButRuleset: - queryable = beatmaps.BeatmapSetsWithoutRuleset; - break; - - case IncludedDetails.AllButFiles: - queryable = beatmaps.BeatmapSetsWithoutFiles; - break; - - default: - queryable = beatmaps.ConsumableItems; - break; - } - - // AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY - // clause which causes queries to take 5-10x longer. - // TODO: remove if upgrading to EF core 3.x. - return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected)); - } + public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSetsEnumerable(includes, includeProtected); /// /// Perform a lookup query on available s. @@ -392,207 +136,207 @@ namespace osu.Game.Beatmaps /// The query. /// The level of detail to include in the returned objects. /// Results from the provided query. - public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) - { - IQueryable queryable; - - switch (includes) - { - case IncludedDetails.Minimal: - queryable = beatmaps.BeatmapSetsOverview; - break; - - case IncludedDetails.AllButRuleset: - queryable = beatmaps.BeatmapSetsWithoutRuleset; - break; - - case IncludedDetails.AllButFiles: - queryable = beatmaps.BeatmapSetsWithoutFiles; - break; - - default: - queryable = beatmaps.ConsumableItems; - break; - } - - return queryable.AsNoTracking().Where(query); - } + public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) => beatmapModelManager.QueryBeatmapSets(query, includes); /// - /// Perform a lookup query on available s. + /// Perform a lookup query on available s. /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapInfo QueryBeatmap(Expression> query) => beatmaps.Beatmaps.AsNoTracking().FirstOrDefault(query); + public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmapModelManager.QueryBeatmapSet(query); /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. - public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); + public IQueryable QueryBeatmaps(Expression> query) => beatmapModelManager.QueryBeatmaps(query); - protected override string HumanisedModelName => "beatmap"; + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapInfo QueryBeatmap(Expression> query) => beatmapModelManager.QueryBeatmap(query); - protected override BeatmapSetInfo CreateModel(ArchiveReader reader) + /// + /// A default representation of a WorkingBeatmap to use when no beatmap is available. + /// + public WorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap; + + /// + /// Fired when a notification should be presented to the user. + /// + public Action PostNotification { - // let's make sure there are actually .osu files to import. - string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); - - if (string.IsNullOrEmpty(mapName)) + set { - Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database); - return null; + beatmapModelManager.PostNotification = value; + beatmapModelDownloader.PostNotification = value; } - - Beatmap beatmap; - using (var stream = new LineBufferedReader(reader.GetStream(mapName))) - beatmap = Decoder.GetDecoder(stream).Decode(stream); - - return new BeatmapSetInfo - { - OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID, - Beatmaps = new List(), - Metadata = beatmap.Metadata, - DateAdded = DateTimeOffset.UtcNow - }; } /// - /// Create all required s for the provided archive. + /// Fired when the user requests to view the resulting import. /// - private List createBeatmapDifficulties(List files) - { - var beatmapInfos = new List(); + public Action>> PresentImport { set => beatmapModelManager.PostImport = value; } - foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) - { - using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath)) - using (var ms = new MemoryStream()) // we need a memory stream so we can seek - using (var sr = new LineBufferedReader(ms)) - { - raw.CopyTo(ms); - ms.Position = 0; + /// + /// Delete a beatmap difficulty. + /// + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmapInfo) => beatmapModelManager.Hide(beatmapInfo); - var decoder = Decoder.GetDecoder(sr); - IBeatmap beatmap = decoder.Decode(sr); - - string hash = ms.ComputeSHA2Hash(); - - if (beatmapInfos.Any(b => b.Hash == hash)) - continue; - - beatmap.BeatmapInfo.Path = file.Filename; - beatmap.BeatmapInfo.Hash = hash; - beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); - - var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); - beatmap.BeatmapInfo.Ruleset = ruleset; - - // TODO: this should be done in a better place once we actually need to dynamically update it. - beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; - beatmap.BeatmapInfo.Length = calculateLength(beatmap); - beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); - - beatmapInfos.Add(beatmap.BeatmapInfo); - } - } - - return beatmapInfos; - } - - private double calculateLength(IBeatmap b) - { - if (!b.HitObjects.Any()) - return 0; - - var lastObject = b.HitObjects.Last(); - - //TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list). - double endTime = lastObject.GetEndTime(); - double startTime = b.HitObjects.First().StartTime; - - return endTime - startTime; - } - - private void removeWorkingCache(BeatmapSetInfo info) - { - if (info.Beatmaps == null) return; - - foreach (var b in info.Beatmaps) - removeWorkingCache(b); - } - - private void removeWorkingCache(BeatmapInfo info) - { - lock (workingCache) - { - var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID); - if (working != null) - workingCache.Remove(working); - } - } - - public void Dispose() - { - onlineLookupQueue?.Dispose(); - } - - #region IResourceStorageProvider - - TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; - ITrackStore IBeatmapResourceProvider.Tracks => trackStore; - AudioManager IStorageResourceProvider.AudioManager => audioManager; - IResourceStore IStorageResourceProvider.Files => Files.Store; - IResourceStore IStorageResourceProvider.Resources => resources; - IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); + /// + /// Restore a beatmap difficulty. + /// + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmapInfo) => beatmapModelManager.Restore(beatmapInfo); #endregion - /// - /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. - /// - private class DummyConversionBeatmap : WorkingBeatmap + #region Implementation of IModelManager + + public bool IsAvailableLocally(BeatmapSetInfo model) { - private readonly IBeatmap beatmap; - - public DummyConversionBeatmap(IBeatmap beatmap) - : base(beatmap.BeatmapInfo, null) - { - this.beatmap = beatmap; - } - - protected override IBeatmap GetBeatmap() => beatmap; - protected override Texture GetBackground() => null; - protected override Track GetBeatmapTrack() => null; - protected internal override ISkin GetSkin() => null; - public override Stream GetStream(string storagePath) => null; + return beatmapModelManager.IsAvailableLocally(model); } - } - /// - /// The level of detail to include in database results. - /// - public enum IncludedDetails - { - /// - /// Only include beatmap difficulties and set level metadata. - /// - Minimal, + public IBindable> ItemUpdated => beatmapModelManager.ItemUpdated; - /// - /// Include all difficulties, rulesets, difficulty metadata but no files. - /// - AllButFiles, + public IBindable> ItemRemoved => beatmapModelManager.ItemRemoved; - /// - /// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap. - /// - AllButRuleset, + public Task ImportFromStableAsync(StableStorage stableStorage) + { + return beatmapModelManager.ImportFromStableAsync(stableStorage); + } - /// - /// Include everything. - /// - All + public void Export(BeatmapSetInfo item) + { + beatmapModelManager.Export(item); + } + + public void ExportModelTo(BeatmapSetInfo model, Stream outputStream) + { + beatmapModelManager.ExportModelTo(model, outputStream); + } + + public void Update(BeatmapSetInfo item) + { + beatmapModelManager.Update(item); + } + + public bool Delete(BeatmapSetInfo item) + { + return beatmapModelManager.Delete(item); + } + + public void Delete(List items, bool silent = false) + { + beatmapModelManager.Delete(items, silent); + } + + public void Undelete(List items, bool silent = false) + { + beatmapModelManager.Undelete(items, silent); + } + + public void Undelete(BeatmapSetInfo item) + { + beatmapModelManager.Undelete(item); + } + + #endregion + + #region Implementation of IModelDownloader + + public IBindable>> DownloadBegan => beatmapModelDownloader.DownloadBegan; + + public IBindable>> DownloadFailed => beatmapModelDownloader.DownloadFailed; + + public bool Download(BeatmapSetInfo model, bool minimiseDownloadSize = false) + { + return beatmapModelDownloader.Download(model, minimiseDownloadSize); + } + + public ArchiveDownloadRequest GetExistingDownload(BeatmapSetInfo model) + { + return beatmapModelDownloader.GetExistingDownload(model); + } + + #endregion + + #region Implementation of ICanAcceptFiles + + public Task Import(params string[] paths) + { + return beatmapModelManager.Import(paths); + } + + public Task Import(params ImportTask[] tasks) + { + return beatmapModelManager.Import(tasks); + } + + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) + { + return beatmapModelManager.Import(notification, tasks); + } + + public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return beatmapModelManager.Import(task, lowPriority, cancellationToken); + } + + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return beatmapModelManager.Import(archive, lowPriority, cancellationToken); + } + + public Task> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken); + } + + public IEnumerable HandledExtensions => beatmapModelManager.HandledExtensions; + + #endregion + + #region Implementation of IWorkingBeatmapCache + + public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); + + void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo); + void IWorkingBeatmapCache.Invalidate(BeatmapInfo beatmapInfo) => workingBeatmapCache.Invalidate(beatmapInfo); + + #endregion + + #region Implementation of IModelFileManager + + public void ReplaceFile(BeatmapSetInfo model, BeatmapSetFileInfo file, Stream contents, string filename = null) + { + beatmapModelManager.ReplaceFile(model, file, contents, filename); + } + + public void DeleteFile(BeatmapSetInfo model, BeatmapSetFileInfo file) + { + beatmapModelManager.DeleteFile(model, file); + } + + public void AddFile(BeatmapSetInfo model, Stream contents, string filename) + { + beatmapModelManager.AddFile(model, contents, filename); + } + + #endregion + + #region Implementation of IDisposable + + public void Dispose() + { + onlineBeatmapLookupQueue?.Dispose(); + } + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs deleted file mode 100644 index 3dd34f6c2f..0000000000 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.Sqlite; -using osu.Framework.Development; -using osu.Framework.IO.Network; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Framework.Testing; -using osu.Framework.Threading; -using osu.Game.Database; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using SharpCompress.Compressors; -using SharpCompress.Compressors.BZip2; - -namespace osu.Game.Beatmaps -{ - public partial class BeatmapManager - { - [ExcludeFromDynamicCompile] - private class BeatmapOnlineLookupQueue : IDisposable - { - private readonly IAPIProvider api; - private readonly Storage storage; - - private const int update_queue_request_concurrency = 4; - - private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapOnlineLookupQueue)); - - private FileWebRequest cacheDownloadRequest; - - private const string cache_database_name = "online.db"; - - public BeatmapOnlineLookupQueue(IAPIProvider api, Storage storage) - { - this.api = api; - this.storage = storage; - - // avoid downloading / using cache for unit tests. - if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name)) - prepareLocalCache(); - } - - public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) - { - return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray()); - } - - // todo: expose this when we need to do individual difficulty lookups. - protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken) - => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); - - private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap) - { - if (checkLocalCache(set, beatmap)) - return; - - if (api?.State.Value != APIState.Online) - return; - - var req = new GetBeatmapRequest(beatmap); - - req.Failure += fail; - - try - { - // intentionally blocking to limit web request concurrency - api.Perform(req); - - var res = req.Result; - - if (res != null) - { - beatmap.Status = res.Status; - beatmap.BeatmapSet.Status = res.BeatmapSet.Status; - beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; - beatmap.OnlineBeatmapID = res.OnlineBeatmapID; - - if (beatmap.Metadata != null) - beatmap.Metadata.AuthorID = res.AuthorID; - - if (beatmap.BeatmapSet.Metadata != null) - beatmap.BeatmapSet.Metadata.AuthorID = res.AuthorID; - - LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); - } - } - catch (Exception e) - { - fail(e); - } - - void fail(Exception e) - { - beatmap.OnlineBeatmapID = null; - LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); - } - } - - private void prepareLocalCache() - { - string cacheFilePath = storage.GetFullPath(cache_database_name); - string compressedCacheFilePath = $"{cacheFilePath}.bz2"; - - cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}"); - - cacheDownloadRequest.Failed += ex => - { - File.Delete(compressedCacheFilePath); - File.Delete(cacheFilePath); - - Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache download failed: {ex}", LoggingTarget.Database); - }; - - cacheDownloadRequest.Finished += () => - { - try - { - using (var stream = File.OpenRead(cacheDownloadRequest.Filename)) - using (var outStream = File.OpenWrite(cacheFilePath)) - using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false)) - bz2.CopyTo(outStream); - - // set to null on completion to allow lookups to begin using the new source - cacheDownloadRequest = null; - } - catch (Exception ex) - { - Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database); - File.Delete(cacheFilePath); - } - finally - { - File.Delete(compressedCacheFilePath); - } - }; - - cacheDownloadRequest.PerformAsync(); - } - - private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmap) - { - // download is in progress (or was, and failed). - if (cacheDownloadRequest != null) - return false; - - // database is unavailable. - if (!storage.Exists(cache_database_name)) - return false; - - if (string.IsNullOrEmpty(beatmap.MD5Hash) - && string.IsNullOrEmpty(beatmap.Path) - && beatmap.OnlineBeatmapID == null) - return false; - - try - { - using (var db = new SqliteConnection(DatabaseContextFactory.CreateDatabaseConnectionString("online.db", storage))) - { - db.Open(); - - using (var cmd = db.CreateCommand()) - { - cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path"; - - cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash)); - cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value)); - cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path)); - - using (var reader = cmd.ExecuteReader()) - { - if (reader.Read()) - { - var status = (BeatmapSetOnlineStatus)reader.GetByte(2); - - beatmap.Status = status; - beatmap.BeatmapSet.Status = status; - beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0); - beatmap.OnlineBeatmapID = reader.GetInt32(1); - - if (beatmap.Metadata != null) - beatmap.Metadata.AuthorID = reader.GetInt32(3); - - if (beatmap.BeatmapSet.Metadata != null) - beatmap.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3); - - LogForModel(set, $"Cached local retrieval for {beatmap}."); - return true; - } - } - } - } - } - catch (Exception ex) - { - LogForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}."); - } - - return false; - } - - public void Dispose() - { - cacheDownloadRequest?.Dispose(); - updateScheduler?.Dispose(); - } - } - } -} diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 713f80d1fe..711533e118 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -4,9 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; using Newtonsoft.Json; -using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Users; @@ -15,7 +13,7 @@ namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [Serializable] - public class BeatmapMetadata : IEquatable, IHasPrimaryKey + public class BeatmapMetadata : IEquatable, IHasPrimaryKey, IBeatmapMetadataInfo { public int ID { get; set; } @@ -83,50 +81,13 @@ namespace osu.Game.Beatmaps public int PreviewTime { get; set; } public string AudioFile { get; set; } + public string BackgroundFile { get; set; } - public override string ToString() - { - string author = Author == null ? string.Empty : $"({Author})"; - return $"{Artist} - {Title} {author}".Trim(); - } + public bool Equals(BeatmapMetadata other) => ((IBeatmapMetadataInfo)this).Equals(other); - public RomanisableString ToRomanisableString() - { - string author = Author == null ? string.Empty : $"({Author})"; - var artistUnicode = string.IsNullOrEmpty(ArtistUnicode) ? Artist : ArtistUnicode; - var titleUnicode = string.IsNullOrEmpty(TitleUnicode) ? Title : TitleUnicode; + public override string ToString() => this.GetDisplayTitle(); - return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{Artist} - {Title} {author}".Trim()); - } - - [JsonIgnore] - public string[] SearchableTerms => new[] - { - Author?.Username, - Artist, - ArtistUnicode, - Title, - TitleUnicode, - Source, - Tags - }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); - - public bool Equals(BeatmapMetadata other) - { - if (other == null) - return false; - - return Title == other.Title - && TitleUnicode == other.TitleUnicode - && Artist == other.Artist - && ArtistUnicode == other.ArtistUnicode - && AuthorString == other.AuthorString - && Source == other.Source - && Tags == other.Tags - && PreviewTime == other.PreviewTime - && AudioFile == other.AudioFile - && BackgroundFile == other.BackgroundFile; - } + string IBeatmapMetadataInfo.Author => AuthorString; } } diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs new file mode 100644 index 0000000000..ee946eeeec --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Localisation; + +namespace osu.Game.Beatmaps +{ + public static class BeatmapMetadataInfoExtensions + { + /// + /// An array of all searchable terms provided in contained metadata. + /// + public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo) => new[] + { + metadataInfo.Author, + metadataInfo.Artist, + metadataInfo.ArtistUnicode, + metadataInfo.Title, + metadataInfo.TitleUnicode, + metadataInfo.Source, + metadataInfo.Tags + }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); + + /// + /// A user-presentable display title representing this metadata. + /// + public static string GetDisplayTitle(this IBeatmapMetadataInfo metadataInfo) + { + string author = string.IsNullOrEmpty(metadataInfo.Author) ? string.Empty : $"({metadataInfo.Author})"; + return $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim(); + } + + /// + /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. + /// + public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapMetadataInfo metadataInfo) + { + string author = string.IsNullOrEmpty(metadataInfo.Author) ? string.Empty : $"({metadataInfo.Author})"; + var artistUnicode = string.IsNullOrEmpty(metadataInfo.ArtistUnicode) ? metadataInfo.Artist : metadataInfo.ArtistUnicode; + var titleUnicode = string.IsNullOrEmpty(metadataInfo.TitleUnicode) ? metadataInfo.Title : metadataInfo.TitleUnicode; + + return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim()); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapModelDownloader.cs b/osu.Game/Beatmaps/BeatmapModelDownloader.cs new file mode 100644 index 0000000000..ae482eeafd --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapModelDownloader.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Beatmaps +{ + public class BeatmapModelDownloader : ModelDownloader + { + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => + new DownloadBeatmapSetRequest(set, minimiseDownloadSize); + + public BeatmapModelDownloader(BeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null) + : base(beatmapModelManager, api, host) + { + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs new file mode 100644 index 0000000000..787559899a --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -0,0 +1,473 @@ +// 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.Linq.Expressions; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Textures; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps.Formats; +using osu.Game.Database; +using osu.Game.IO; +using osu.Game.IO.Archives; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Skinning; +using Decoder = osu.Game.Beatmaps.Formats.Decoder; + +namespace osu.Game.Beatmaps +{ + /// + /// Handles ef-core storage of beatmaps. + /// + [ExcludeFromDynamicCompile] + public class BeatmapModelManager : ArchiveModelManager, IBeatmapModelManager + { + /// + /// Fired when a single difficulty has been hidden. + /// + public IBindable> BeatmapHidden => beatmapHidden; + + private readonly Bindable> beatmapHidden = new Bindable>(); + + /// + /// Fired when a single difficulty has been restored. + /// + public IBindable> BeatmapRestored => beatmapRestored; + + /// + /// An online lookup queue component which handles populating online beatmap metadata. + /// + public BeatmapOnlineLookupQueue OnlineLookupQueue { private get; set; } + + /// + /// The game working beatmap cache, used to invalidate entries on changes. + /// + public IWorkingBeatmapCache WorkingBeatmapCache { private get; set; } + + private readonly Bindable> beatmapRestored = new Bindable>(); + + public override IEnumerable HandledExtensions => new[] { ".osz" }; + + protected override string[] HashableFileTypes => new[] { ".osu" }; + + protected override string ImportFromStablePath => "."; + + protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); + + private readonly BeatmapStore beatmaps; + private readonly RulesetStore rulesets; + + public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, GameHost host = null) + : base(storage, contextFactory, new BeatmapStore(contextFactory), host) + { + this.rulesets = rulesets; + + beatmaps = (BeatmapStore)ModelStore; + beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference(b); + beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference(b); + beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b); + beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); + } + + protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; + + protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) + { + if (archive != null) + beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files); + + foreach (BeatmapInfo b in beatmapSet.Beatmaps) + { + // remove metadata from difficulties where it matches the set + if (beatmapSet.Metadata.Equals(b.Metadata)) + b.Metadata = null; + + b.BeatmapSet = beatmapSet; + } + + validateOnlineIds(beatmapSet); + + bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); + + if (OnlineLookupQueue != null) + await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); + + // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. + if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) + { + if (beatmapSet.OnlineBeatmapSetID != null) + { + beatmapSet.OnlineBeatmapSetID = null; + LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); + } + } + } + + protected override void PreImport(BeatmapSetInfo beatmapSet) + { + if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null)) + throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}."); + + // check if a set already exists with the same online id, delete if it does. + if (beatmapSet.OnlineBeatmapSetID != null) + { + var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); + + if (existingOnlineId != null) + { + Delete(existingOnlineId); + + // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. + existingOnlineId.OnlineBeatmapSetID = null; + foreach (var b in existingOnlineId.Beatmaps) + b.OnlineBeatmapID = null; + + LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted."); + } + } + } + + private void validateOnlineIds(BeatmapSetInfo beatmapSet) + { + var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); + + // ensure all IDs are unique + if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) + { + LogForModel(beatmapSet, "Found non-unique IDs, resetting..."); + resetIds(); + return; + } + + // find any existing beatmaps in the database that have matching online ids + var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList(); + + if (existingBeatmaps.Count > 0) + { + // reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set. + // we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted. + var existing = CheckForExisting(beatmapSet); + + if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b))) + { + LogForModel(beatmapSet, "Found existing import with IDs already, resetting..."); + resetIds(); + } + } + + void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); + } + + /// + /// Delete a beatmap difficulty. + /// + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmapInfo) => beatmaps.Hide(beatmapInfo); + + /// + /// Restore a beatmap difficulty. + /// + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmapInfo) => beatmaps.Restore(beatmapInfo); + + /// + /// Saves an file against a given . + /// + /// The to save the content against. The file referenced by will be replaced. + /// The content to write. + /// The beatmap content to write, null if to be omitted. + public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin beatmapSkin = null) + { + var setInfo = beatmapInfo.BeatmapSet; + + using (var stream = new MemoryStream()) + { + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + 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). + var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); + + // metadata may have changed; update the path with the standard format. + beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu"; + beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); + + // update existing or populate new file's filename. + fileInfo.Filename = beatmapInfo.Path; + + stream.Seek(0, SeekOrigin.Begin); + ReplaceFile(setInfo, fileInfo, stream); + } + } + + WorkingBeatmapCache?.Invalidate(beatmapInfo); + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); + + protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import) + { + if (!base.CanSkipImport(existing, import)) + return false; + + return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null); + } + + protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) + { + if (!base.CanReuseExisting(existing, import)) + return false; + + var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); + var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); + + // force re-import if we are not in a sane state. + return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds); + } + + /// + /// Returns a list of all usable s. + /// + /// A list of available . + public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => + GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList(); + + /// + /// Returns a list of all usable s. Note that files are not populated. + /// + /// The level of detail to include in the returned objects. + /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. + /// A list of available . + public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) + { + IQueryable queryable; + + switch (includes) + { + case IncludedDetails.Minimal: + queryable = beatmaps.BeatmapSetsOverview; + break; + + case IncludedDetails.AllButRuleset: + queryable = beatmaps.BeatmapSetsWithoutRuleset; + break; + + case IncludedDetails.AllButFiles: + queryable = beatmaps.BeatmapSetsWithoutFiles; + break; + + default: + queryable = beatmaps.ConsumableItems; + break; + } + + // AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY + // clause which causes queries to take 5-10x longer. + // TODO: remove if upgrading to EF core 3.x. + return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected)); + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The level of detail to include in the returned objects. + /// Results from the provided query. + public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) + { + IQueryable queryable; + + switch (includes) + { + case IncludedDetails.Minimal: + queryable = beatmaps.BeatmapSetsOverview; + break; + + case IncludedDetails.AllButRuleset: + queryable = beatmaps.BeatmapSetsWithoutRuleset; + break; + + case IncludedDetails.AllButFiles: + queryable = beatmaps.BeatmapSetsWithoutFiles; + break; + + default: + queryable = beatmaps.ConsumableItems; + break; + } + + return queryable.AsNoTracking().Where(query); + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapInfo QueryBeatmap(Expression> query) => beatmaps.Beatmaps.AsNoTracking().FirstOrDefault(query); + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// Results from the provided query. + public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); + + public override string HumanisedModelName => "beatmap"; + + protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) + => base.CheckLocalAvailability(model, items) + || (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID)); + + protected override BeatmapSetInfo CreateModel(ArchiveReader reader) + { + // let's make sure there are actually .osu files to import. + string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); + + if (string.IsNullOrEmpty(mapName)) + { + Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database); + return null; + } + + Beatmap beatmap; + using (var stream = new LineBufferedReader(reader.GetStream(mapName))) + beatmap = Decoder.GetDecoder(stream).Decode(stream); + + return new BeatmapSetInfo + { + OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID, + Beatmaps = new List(), + Metadata = beatmap.Metadata, + DateAdded = DateTimeOffset.UtcNow + }; + } + + /// + /// Create all required s for the provided archive. + /// + private List createBeatmapDifficulties(List files) + { + var beatmapInfos = new List(); + + foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) + { + using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath)) + using (var ms = new MemoryStream()) // we need a memory stream so we can seek + using (var sr = new LineBufferedReader(ms)) + { + raw.CopyTo(ms); + ms.Position = 0; + + var decoder = Decoder.GetDecoder(sr); + IBeatmap beatmap = decoder.Decode(sr); + + string hash = ms.ComputeSHA2Hash(); + + if (beatmapInfos.Any(b => b.Hash == hash)) + continue; + + beatmap.BeatmapInfo.Path = file.Filename; + beatmap.BeatmapInfo.Hash = hash; + beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); + + var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); + beatmap.BeatmapInfo.Ruleset = ruleset; + + // TODO: this should be done in a better place once we actually need to dynamically update it. + beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; + beatmap.BeatmapInfo.Length = calculateLength(beatmap); + beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); + + beatmapInfos.Add(beatmap.BeatmapInfo); + } + } + + return beatmapInfos; + } + + private double calculateLength(IBeatmap b) + { + if (!b.HitObjects.Any()) + return 0; + + var lastObject = b.HitObjects.Last(); + + //TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list). + double endTime = lastObject.GetEndTime(); + double startTime = b.HitObjects.First().StartTime; + + return endTime - startTime; + } + + /// + /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. + /// + private class DummyConversionBeatmap : WorkingBeatmap + { + private readonly IBeatmap beatmap; + + public DummyConversionBeatmap(IBeatmap beatmap) + : base(beatmap.BeatmapInfo, null) + { + this.beatmap = beatmap; + } + + protected override IBeatmap GetBeatmap() => beatmap; + protected override Texture GetBackground() => null; + protected override Track GetBeatmapTrack() => null; + protected internal override ISkin GetSkin() => null; + public override Stream GetStream(string storagePath) => null; + } + } + + /// + /// The level of detail to include in database results. + /// + public enum IncludedDetails + { + /// + /// Only include beatmap difficulties and set level metadata. + /// + Minimal, + + /// + /// Include all difficulties, rulesets, difficulty metadata but no files. + /// + AllButFiles, + + /// + /// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap. + /// + AllButRuleset, + + /// + /// Include everything. + /// + All + } +} diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs new file mode 100644 index 0000000000..1fe120557d --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -0,0 +1,222 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using osu.Framework.Development; +using osu.Framework.IO.Network; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Framework.Threading; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using SharpCompress.Compressors; +using SharpCompress.Compressors.BZip2; + +namespace osu.Game.Beatmaps +{ + /// + /// A component which handles population of online IDs for beatmaps using a two part lookup procedure. + /// + /// + /// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to ) will be downloaded if not already present locally. + /// This will always be checked before doing a second online query to get required metadata. + /// + [ExcludeFromDynamicCompile] + public class BeatmapOnlineLookupQueue : IDisposable + { + private readonly IAPIProvider api; + private readonly Storage storage; + + private const int update_queue_request_concurrency = 4; + + private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapOnlineLookupQueue)); + + private FileWebRequest cacheDownloadRequest; + + private const string cache_database_name = "online.db"; + + public BeatmapOnlineLookupQueue(IAPIProvider api, Storage storage) + { + this.api = api; + this.storage = storage; + + // avoid downloading / using cache for unit tests. + if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name)) + prepareLocalCache(); + } + + public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) + { + return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray()); + } + + // todo: expose this when we need to do individual difficulty lookups. + protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmapInfo, CancellationToken cancellationToken) + => Task.Factory.StartNew(() => lookup(beatmapSet, beatmapInfo), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + + private void lookup(BeatmapSetInfo set, BeatmapInfo beatmapInfo) + { + if (checkLocalCache(set, beatmapInfo)) + return; + + if (api?.State.Value != APIState.Online) + return; + + var req = new GetBeatmapRequest(beatmapInfo); + + req.Failure += fail; + + try + { + // intentionally blocking to limit web request concurrency + api.Perform(req); + + var res = req.Response; + + if (res != null) + { + beatmapInfo.Status = res.Status; + beatmapInfo.BeatmapSet.Status = res.BeatmapSet.Status; + beatmapInfo.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; + beatmapInfo.OnlineBeatmapID = res.OnlineBeatmapID; + + if (beatmapInfo.Metadata != null) + beatmapInfo.Metadata.AuthorID = res.AuthorID; + + if (beatmapInfo.BeatmapSet.Metadata != null) + beatmapInfo.BeatmapSet.Metadata.AuthorID = res.AuthorID; + + logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + } + } + catch (Exception e) + { + fail(e); + } + + void fail(Exception e) + { + beatmapInfo.OnlineBeatmapID = null; + logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})"); + } + } + + private void prepareLocalCache() + { + string cacheFilePath = storage.GetFullPath(cache_database_name); + string compressedCacheFilePath = $"{cacheFilePath}.bz2"; + + cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}"); + + cacheDownloadRequest.Failed += ex => + { + File.Delete(compressedCacheFilePath); + File.Delete(cacheFilePath); + + Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache download failed: {ex}", LoggingTarget.Database); + }; + + cacheDownloadRequest.Finished += () => + { + try + { + using (var stream = File.OpenRead(cacheDownloadRequest.Filename)) + using (var outStream = File.OpenWrite(cacheFilePath)) + using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false)) + bz2.CopyTo(outStream); + + // set to null on completion to allow lookups to begin using the new source + cacheDownloadRequest = null; + } + catch (Exception ex) + { + Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database); + File.Delete(cacheFilePath); + } + finally + { + File.Delete(compressedCacheFilePath); + } + }; + + cacheDownloadRequest.PerformAsync(); + } + + private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo) + { + // download is in progress (or was, and failed). + if (cacheDownloadRequest != null) + return false; + + // database is unavailable. + if (!storage.Exists(cache_database_name)) + return false; + + if (string.IsNullOrEmpty(beatmapInfo.MD5Hash) + && string.IsNullOrEmpty(beatmapInfo.Path) + && beatmapInfo.OnlineBeatmapID == null) + return false; + + try + { + using (var db = new SqliteConnection(DatabaseContextFactory.CreateDatabaseConnectionString("online.db", storage))) + { + db.Open(); + + using (var cmd = db.CreateCommand()) + { + cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path"; + + cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash)); + cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmapInfo.OnlineBeatmapID ?? (object)DBNull.Value)); + cmd.Parameters.Add(new SqliteParameter("@Path", beatmapInfo.Path)); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + var status = (BeatmapSetOnlineStatus)reader.GetByte(2); + + beatmapInfo.Status = status; + beatmapInfo.BeatmapSet.Status = status; + beatmapInfo.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0); + beatmapInfo.OnlineBeatmapID = reader.GetInt32(1); + + if (beatmapInfo.Metadata != null) + beatmapInfo.Metadata.AuthorID = reader.GetInt32(3); + + if (beatmapInfo.BeatmapSet.Metadata != null) + beatmapInfo.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3); + + logForModel(set, $"Cached local retrieval for {beatmapInfo}."); + return true; + } + } + } + } + } + catch (Exception ex) + { + logForModel(set, $"Cached local retrieval for {beatmapInfo} failed with {ex}."); + } + + return false; + } + + private void logForModel(BeatmapSetInfo set, string message) => + ArchiveModelManager.LogForModel(set, $"[{nameof(BeatmapOnlineLookupQueue)}] {message}"); + + public void Dispose() + { + cacheDownloadRequest?.Dispose(); + updateScheduler?.Dispose(); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs index 3a55dc1577..ce50463f05 100644 --- a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs @@ -7,7 +7,7 @@ using osu.Game.IO; namespace osu.Game.Beatmaps { - public class BeatmapSetFileInfo : INamedFileInfo, IHasPrimaryKey + public class BeatmapSetFileInfo : INamedFileInfo, IHasPrimaryKey, INamedFileUsage { public int ID { get; set; } @@ -19,5 +19,7 @@ namespace osu.Game.Beatmaps [Required] public string Filename { get; set; } + + public IFileInfo File => FileInfo; } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 3b1ff4ced0..8b01831b3c 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -12,7 +12,7 @@ using osu.Game.Database; namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] - public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable + public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable, IBeatmapSetInfo { public int ID { get; set; } @@ -61,8 +61,6 @@ namespace osu.Game.Beatmaps public string Hash { get; set; } - public string StoryboardFile => Files.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename; - /// /// 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. @@ -90,5 +88,19 @@ namespace osu.Game.Beatmaps return ReferenceEquals(this, other); } + + #region Implementation of IHasOnlineID + + public int? OnlineID => OnlineBeatmapSetID; + + #endregion + + #region Implementation of IBeatmapSetInfo + + IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => Metadata; + IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; + IEnumerable IBeatmapSetInfo.Files => Files; + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index e3214b7c03..197581db88 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -25,40 +25,40 @@ namespace osu.Game.Beatmaps /// /// Hide a in the database. /// - /// The beatmap to hide. + /// The beatmap to hide. /// Whether the beatmap's was changed. - public bool Hide(BeatmapInfo beatmap) + public bool Hide(BeatmapInfo beatmapInfo) { using (ContextFactory.GetForWrite()) { - Refresh(ref beatmap, Beatmaps); + Refresh(ref beatmapInfo, Beatmaps); - if (beatmap.Hidden) return false; + if (beatmapInfo.Hidden) return false; - beatmap.Hidden = true; + beatmapInfo.Hidden = true; } - BeatmapHidden?.Invoke(beatmap); + BeatmapHidden?.Invoke(beatmapInfo); return true; } /// /// Restore a previously hidden . /// - /// The beatmap to restore. + /// The beatmap to restore. /// Whether the beatmap's was changed. - public bool Restore(BeatmapInfo beatmap) + public bool Restore(BeatmapInfo beatmapInfo) { using (ContextFactory.GetForWrite()) { - Refresh(ref beatmap, Beatmaps); + Refresh(ref beatmapInfo, Beatmaps); - if (!beatmap.Hidden) return false; + if (!beatmapInfo.Hidden) return false; - beatmap.Hidden = false; + beatmapInfo.Hidden = false; } - BeatmapRestored?.Invoke(beatmap); + BeatmapRestored?.Invoke(beatmapInfo); return true; } diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index ca910e70b8..b1b1e58ab7 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -62,14 +62,14 @@ namespace osu.Game.Beatmaps if (!recommendedDifficultyMapping.TryGetValue(r, out var recommendation)) continue; - BeatmapInfo beatmap = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => + BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => { var difference = b.StarDifficulty - recommendation; return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder }).FirstOrDefault(); - if (beatmap != null) - return beatmap; + if (beatmapInfo != null) + return beatmapInfo; } return null; diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 0751a777d8..880d70aec2 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -37,7 +37,7 @@ namespace osu.Game.Beatmaps.Drawables } [NotNull] - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; [CanBeNull] private readonly RulesetInfo ruleset; @@ -56,26 +56,26 @@ namespace osu.Game.Beatmaps.Drawables /// /// Creates a new with a given and combination. /// - /// The beatmap to show the difficulty of. + /// The beatmap to show the difficulty of. /// The ruleset to show the difficulty with. /// The mods to show the difficulty with. /// Whether to display a tooltip when hovered. - public DifficultyIcon([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true) - : this(beatmap, shouldShowTooltip) + public DifficultyIcon([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true) + : this(beatmapInfo, shouldShowTooltip) { - this.ruleset = ruleset ?? beatmap.Ruleset; + this.ruleset = ruleset ?? beatmapInfo.Ruleset; this.mods = mods ?? Array.Empty(); } /// /// Creates a new that follows the currently-selected ruleset and mods. /// - /// The beatmap to show the difficulty of. + /// The beatmap to show the difficulty of. /// Whether to display a tooltip when hovered. /// Whether to perform difficulty lookup (including calculation if necessary). - public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true) + public DifficultyIcon([NotNull] BeatmapInfo beatmapInfo, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true) { - this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap)); + this.beatmapInfo = beatmapInfo ?? throw new ArgumentNullException(nameof(beatmapInfo)); this.shouldShowTooltip = shouldShowTooltip; this.performBackgroundDifficultyLookup = performBackgroundDifficultyLookup; @@ -105,7 +105,7 @@ namespace osu.Game.Beatmaps.Drawables Child = background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.ForStarDifficulty(beatmap.StarDifficulty) // Default value that will be re-populated once difficulty calculation completes + Colour = colours.ForStarDifficulty(beatmapInfo.StarDifficulty) // Default value that will be re-populated once difficulty calculation completes }, }, new ConstrainedIconContainer @@ -114,27 +114,27 @@ namespace osu.Game.Beatmaps.Drawables Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) - Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } + Icon = (ruleset ?? beatmapInfo.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } }, }; if (performBackgroundDifficultyLookup) - iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0)); + iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmapInfo, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0)); else - difficultyBindable.Value = new StarDifficulty(beatmap.StarDifficulty, 0); + difficultyBindable.Value = new StarDifficulty(beatmapInfo.StarDifficulty, 0); difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars)); } ITooltip IHasCustomTooltip.GetCustomTooltip() => new DifficultyIconTooltip(); - DifficultyIconTooltipContent IHasCustomTooltip.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmap, difficultyBindable) : null; + DifficultyIconTooltipContent IHasCustomTooltip.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmapInfo, difficultyBindable) : null; private class DifficultyRetriever : Component { public readonly Bindable StarDifficulty = new Bindable(); - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private readonly RulesetInfo ruleset; private readonly IReadOnlyList mods; @@ -143,9 +143,9 @@ namespace osu.Game.Beatmaps.Drawables [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } - public DifficultyRetriever(BeatmapInfo beatmap, RulesetInfo ruleset, IReadOnlyList mods) + public DifficultyRetriever(BeatmapInfo beatmapInfo, RulesetInfo ruleset, IReadOnlyList mods) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; this.ruleset = ruleset; this.mods = mods; } @@ -157,8 +157,8 @@ namespace osu.Game.Beatmaps.Drawables { difficultyCancellation = new CancellationTokenSource(); localStarDifficulty = ruleset != null - ? difficultyCache.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token) - : difficultyCache.GetBindableDifficulty(beatmap, difficultyCancellation.Token); + ? difficultyCache.GetBindableDifficulty(beatmapInfo, ruleset, mods, difficultyCancellation.Token) + : difficultyCache.GetBindableDifficulty(beatmapInfo, difficultyCancellation.Token); localStarDifficulty.BindValueChanged(d => { if (d.NewValue is StarDifficulty diff) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs index 0329e935bc..d4c9f83a0a 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs @@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps.Drawables public void SetContent(DifficultyIconTooltipContent content) { - difficultyName.Text = content.Beatmap.Version; + difficultyName.Text = content.BeatmapInfo.Version; starDifficulty.UnbindAll(); starDifficulty.BindTo(content.Difficulty); @@ -109,12 +109,12 @@ namespace osu.Game.Beatmaps.Drawables internal class DifficultyIconTooltipContent { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public readonly IBindable Difficulty; - public DifficultyIconTooltipContent(BeatmapInfo beatmap, IBindable difficulty) + public DifficultyIconTooltipContent(BeatmapInfo beatmapInfo, IBindable difficulty) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; Difficulty = difficulty; } } diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 6301c42deb..0f15e28c00 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -176,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats case "L": { var startTime = Parsing.ParseDouble(split[1]); - var loopCount = Parsing.ParseInt(split[2]); - timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount); + var repeatCount = Parsing.ParseInt(split[2]); + timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount - 1)); break; } diff --git a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs new file mode 100644 index 0000000000..339364d442 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A representation of all top-level difficulty settings for a beatmap. + /// + public interface IBeatmapDifficultyInfo + { + /// + /// The default value used for all difficulty settings except and . + /// + const float DEFAULT_DIFFICULTY = 5; + + /// + /// The drain rate of the associated beatmap. + /// + float DrainRate { get; } + + /// + /// The circle size of the associated beatmap. + /// + float CircleSize { get; } + + /// + /// The overall difficulty of the associated beatmap. + /// + float OverallDifficulty { get; } + + /// + /// The approach rate of the associated beatmap. + /// + float ApproachRate { get; } + + /// + /// The slider multiplier of the associated beatmap. + /// + double SliderMultiplier { get; } + + /// + /// The slider tick rate of the associated beatmap. + /// + double SliderTickRate { get; } + + /// + /// Maps a difficulty value [0, 10] to a two-piece linear range of values. + /// + /// The difficulty value to be mapped. + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. + /// Maximum of the resulting range which will be achieved by a difficulty value of 10. + /// Value to which the difficulty value maps in the specified range. + static double DifficultyRange(double difficulty, double min, double mid, double max) + { + if (difficulty > 5) + return mid + (max - mid) * (difficulty - 5) / 5; + if (difficulty < 5) + return mid - (mid - min) * (5 - difficulty) / 5; + + return mid; + } + + /// + /// Maps a difficulty value [0, 10] to a two-piece linear range of values. + /// + /// The difficulty value to be mapped. + /// The values that define the two linear ranges. + /// + /// + /// od0 + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// + /// + /// od5 + /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. + /// + /// + /// od10 + /// Maximum of the resulting range which will be achieved by a difficulty value of 10. + /// + /// + /// + /// Value to which the difficulty value maps in the specified range. + static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) + => DifficultyRange(difficulty, range.od0, range.od5, range.od10); + } +} diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs new file mode 100644 index 0000000000..3d51c5d4b6 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -0,0 +1,66 @@ +// 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.Database; +using osu.Game.Rulesets; + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A single beatmap difficulty. + /// + public interface IBeatmapInfo : IHasOnlineID + { + /// + /// The user-specified name given to this beatmap. + /// + string DifficultyName { get; } + + /// + /// The metadata representing this beatmap. May be shared between multiple beatmaps. + /// + IBeatmapMetadataInfo? Metadata { get; } + + /// + /// The difficulty settings for this beatmap. + /// + IBeatmapDifficultyInfo Difficulty { get; } + + /// + /// The beatmap set this beatmap is part of. + /// + IBeatmapSetInfo? BeatmapSet { get; } + + /// + /// The playable length in milliseconds of this beatmap. + /// + double Length { get; } + + /// + /// The most common BPM of this beatmap. + /// + double BPM { get; } + + /// + /// The SHA-256 hash representing this beatmap's contents. + /// + string Hash { get; } + + /// + /// MD5 is kept for legacy support (matching against replays etc.). + /// + string MD5Hash { get; } + + /// + /// The ruleset this beatmap was made for. + /// + IRulesetInfo Ruleset { get; } + + /// + /// The basic star rating for this beatmap (with no mods applied). + /// + double StarRating { get; } + } +} diff --git a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs new file mode 100644 index 0000000000..55aee7d7bc --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// Metadata representing a beatmap. May be shared between multiple beatmap difficulties. + /// + public interface IBeatmapMetadataInfo : IEquatable + { + /// + /// The romanised title of this beatmap. + /// + string Title { get; } + + /// + /// The unicode title of this beatmap. + /// + string TitleUnicode { get; } + + /// + /// The romanised artist of this beatmap. + /// + string Artist { get; } + + /// + /// The unicode artist of this beatmap. + /// + string ArtistUnicode { get; } + + /// + /// The author of this beatmap. + /// + string Author { get; } // eventually should be linked to a persisted User. + + /// + /// The source of this beatmap. + /// + string Source { get; } + + /// + /// The tags of this beatmap. + /// + string Tags { get; } + + /// + /// The time in milliseconds to begin playing the track for preview purposes. + /// If -1, the track should begin playing at 40% of its length. + /// + int PreviewTime { get; } + + /// + /// The filename of the audio file consumed by this beatmap. + /// + string AudioFile { get; } + + /// + /// The filename of the background image file consumed by this beatmap. + /// + string BackgroundFile { get; } + + bool IEquatable.Equals(IBeatmapMetadataInfo? other) + { + if (other == null) + return false; + + return Title == other.Title + && TitleUnicode == other.TitleUnicode + && Artist == other.Artist + && ArtistUnicode == other.ArtistUnicode + && Author == other.Author + && Source == other.Source + && Tags == other.Tags + && PreviewTime == other.PreviewTime + && AudioFile == other.AudioFile + && BackgroundFile == other.BackgroundFile; + } + } +} diff --git a/osu.Game/Beatmaps/IBeatmapModelManager.cs b/osu.Game/Beatmaps/IBeatmapModelManager.cs new file mode 100644 index 0000000000..8c243c2b77 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapModelManager.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Database; + +namespace osu.Game.Beatmaps +{ + public interface IBeatmapModelManager : IModelManager + { + /// + /// Provide an online lookup queue component to handle populating online beatmap metadata. + /// + BeatmapOnlineLookupQueue OnlineLookupQueue { set; } + + /// + /// Provide a working beatmap cache, used to invalidate entries on changes. + /// + IWorkingBeatmapCache WorkingBeatmapCache { set; } + } +} diff --git a/osu.Game/Beatmaps/IBeatmapSetInfo.cs b/osu.Game/Beatmaps/IBeatmapSetInfo.cs new file mode 100644 index 0000000000..0cfb0c4242 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapSetInfo.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Database; + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A representation of a collection of beatmap difficulties, generally packaged as an ".osz" archive. + /// + public interface IBeatmapSetInfo : IHasOnlineID + { + /// + /// The date when this beatmap was imported. + /// + DateTimeOffset DateAdded { get; } + + /// + /// The best-effort metadata representing this set. In the case metadata differs between contained beatmaps, one is arbitrarily chosen. + /// + IBeatmapMetadataInfo? Metadata { get; } + + /// + /// All beatmaps contained in this set. + /// + IEnumerable Beatmaps { get; } + + /// + /// All files used by this set. + /// + IEnumerable Files { get; } + + /// + /// The maximum star difficulty of all beatmaps in this set. + /// + double MaxStarDifficulty { get; } + + /// + /// The maximum playable length in milliseconds of all beatmaps in this set. + /// + double MaxLength { get; } + + /// + /// The maximum BPM of all beatmaps in this set. + /// + double MaxBPM { get; } + } +} diff --git a/osu.Game/Beatmaps/IWorkingBeatmapCache.cs b/osu.Game/Beatmaps/IWorkingBeatmapCache.cs new file mode 100644 index 0000000000..3eb33f10d6 --- /dev/null +++ b/osu.Game/Beatmaps/IWorkingBeatmapCache.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Beatmaps +{ + public interface IWorkingBeatmapCache + { + /// + /// Retrieve a instance for the provided + /// + /// The beatmap to lookup. + /// A instance correlating to the provided . + WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo); + + /// + /// Invalidate a cache entry if it exists. + /// + /// The beatmap set info to invalidate any cached entries for. + void Invalidate(BeatmapSetInfo beatmapSetInfo); + + /// + /// Invalidate a cache entry if it exists. + /// + /// The beatmap info to invalidate any cached entries for. + void Invalidate(BeatmapInfo beatmapInfo); + } +} diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 61760e69b0..c4c5c89f28 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps /// The applicable . protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap); - public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) + public virtual IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) { using (var cancellationSource = createCancellationTokenSource(timeout)) { diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs similarity index 53% rename from osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs rename to osu.Game/Beatmaps/WorkingBeatmapCache.cs index 45112ae74c..ad3e890b3a 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -1,12 +1,18 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Framework.Lists; using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.IO; @@ -15,8 +21,96 @@ using osu.Game.Storyboards; namespace osu.Game.Beatmaps { - public partial class BeatmapManager + public class WorkingBeatmapCache : IBeatmapResourceProvider, IWorkingBeatmapCache { + private readonly WeakList workingCache = new WeakList(); + + /// + /// A default representation of a WorkingBeatmap to use when no beatmap is available. + /// + public readonly WorkingBeatmap DefaultBeatmap; + + public BeatmapModelManager BeatmapManager { private get; set; } + + private readonly AudioManager audioManager; + private readonly IResourceStore resources; + private readonly LargeTextureStore largeTextureStore; + private readonly ITrackStore trackStore; + private readonly IResourceStore files; + + [CanBeNull] + private readonly GameHost host; + + public WorkingBeatmapCache([NotNull] AudioManager audioManager, IResourceStore resources, IResourceStore files, WorkingBeatmap defaultBeatmap = null, GameHost host = null) + { + DefaultBeatmap = defaultBeatmap; + + this.audioManager = audioManager; + this.resources = resources; + this.host = host; + this.files = files; + largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(files)); + trackStore = audioManager.GetTrackStore(files); + } + + public void Invalidate(BeatmapSetInfo info) + { + if (info.Beatmaps == null) return; + + foreach (var b in info.Beatmaps) + Invalidate(b); + } + + public void Invalidate(BeatmapInfo info) + { + lock (workingCache) + { + var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID); + if (working != null) + workingCache.Remove(working); + } + } + + public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) + { + // if there are no files, presume the full beatmap info has not yet been fetched from the database. + if (beatmapInfo?.BeatmapSet?.Files.Count == 0) + { + int lookupId = beatmapInfo.ID; + beatmapInfo = BeatmapManager.QueryBeatmap(b => b.ID == lookupId); + } + + if (beatmapInfo?.BeatmapSet == null) + return DefaultBeatmap; + + lock (workingCache) + { + var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); + if (working != null) + return working; + + beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; + + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); + + // best effort; may be higher than expected. + GlobalStatistics.Get(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count(); + + return working; + } + } + + #region IResourceStorageProvider + + TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; + ITrackStore IBeatmapResourceProvider.Tracks => trackStore; + AudioManager IStorageResourceProvider.AudioManager => audioManager; + IResourceStore IStorageResourceProvider.Files => files; + IResourceStore IStorageResourceProvider.Resources => resources; + IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); + + #endregion + [ExcludeFromDynamicCompile] private class BeatmapManagerWorkingBeatmap : WorkingBeatmap { @@ -107,12 +201,14 @@ namespace osu.Game.Beatmaps { var decoder = Decoder.GetDecoder(stream); + var storyboardFilename = BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename; + // todo: support loading from both set-wide storyboard *and* beatmap specific. - if (BeatmapSetInfo?.StoryboardFile == null) + if (string.IsNullOrEmpty(storyboardFilename)) storyboard = decoder.Decode(stream); else { - using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile)))) + using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(storyboardFilename)))) storyboard = decoder.Decode(stream, secondaryStream); } } diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index fe04c70d62..6f9d9cd8a8 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.IO; using osu.Game.IO.Legacy; using osu.Game.Overlays.Notifications; @@ -27,7 +28,7 @@ namespace osu.Game.Collections /// This is currently reading and writing from the osu-stable file format. This is a temporary arrangement until we refactor the /// database backing the game. Going forward writing should be done in a similar way to other model stores. /// - public class CollectionManager : Component + public class CollectionManager : Component, IPostNotifications { /// /// Database version in stable-compatible YYYYMMDD format. @@ -106,9 +107,6 @@ namespace osu.Game.Collections backgroundSave(); }); - /// - /// Set an endpoint for notifications to be posted to. - /// public Action PostNotification { protected get; set; } /// diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ddd2bc5d1e..ee1a7e2900 100644 --- 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 + public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager, IPostImports where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : class, INamedFileInfo, new() { @@ -57,9 +57,6 @@ namespace osu.Game.Database /// private static readonly ThreadedTaskScheduler import_scheduler_low_priority = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager)); - /// - /// Set an endpoint for notifications to be posted to. - /// public Action PostNotification { protected get; set; } /// @@ -135,13 +132,13 @@ namespace osu.Game.Database return Import(notification, tasks); } - protected async Task> Import(ProgressNotification notification, params ImportTask[] tasks) + public async Task>> Import(ProgressNotification notification, params ImportTask[] tasks) { if (tasks.Length == 0) { notification.CompletionText = $"No {HumanisedModelName}s were found to import!"; notification.State = ProgressNotificationState.Completed; - return Enumerable.Empty(); + return Enumerable.Empty>(); } notification.Progress = 0; @@ -149,7 +146,7 @@ namespace osu.Game.Database int current = 0; - var imported = new List(); + var imported = new List>(); bool isLowPriorityImport = tasks.Length > low_priority_import_batch_size; @@ -200,15 +197,15 @@ namespace osu.Game.Database else { notification.CompletionText = imported.Count == 1 - ? $"Imported {imported.First()}!" + ? $"Imported {imported.First().Value}!" : $"Imported {imported.Count} {HumanisedModelName}s!"; - if (imported.Count > 0 && PresentImport != null) + if (imported.Count > 0 && PostImport != null) { notification.CompletionText += " Click to view."; notification.CompletionClickAction = () => { - PresentImport?.Invoke(imported); + PostImport?.Invoke(imported); return true; }; } @@ -227,11 +224,11 @@ namespace osu.Game.Database /// Whether this is a low priority import. /// An optional cancellation token. /// The imported model, if successful. - internal async Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + public async Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - TModel import; + ILive import; using (ArchiveReader reader = task.GetReader()) import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false); @@ -246,16 +243,13 @@ namespace osu.Game.Database } catch (Exception e) { - LogForModel(import, $@"Could not delete original file after import ({task})", e); + LogForModel(import?.Value, $@"Could not delete original file after import ({task})", e); } return import; } - /// - /// Fired when the user requests to view the resulting import. - /// - public Action> PresentImport; + public Action>> PostImport { protected get; set; } /// /// Silently import an item from an . @@ -263,7 +257,7 @@ namespace osu.Game.Database /// The archive to be imported. /// Whether this is a low priority import. /// An optional cancellation token. - public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -274,7 +268,7 @@ namespace osu.Game.Database model = CreateModel(archive); if (model == null) - return Task.FromResult(null); + return Task.FromResult>(new EntityFrameworkLive(null)); } catch (TaskCanceledException) { @@ -349,7 +343,7 @@ namespace osu.Game.Database /// An optional archive to use for model population. /// Whether this is a low priority import. /// An optional cancellation token. - public virtual async Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => + public virtual async Task> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => { cancellationToken.ThrowIfCancellationRequested(); @@ -375,7 +369,7 @@ namespace osu.Game.Database { LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); Undelete(existing); - return existing; + return existing.ToEntityFrameworkLive(); } LogForModel(item, @"Found existing (optimised) but failed pre-check."); @@ -421,7 +415,7 @@ namespace osu.Game.Database // existing item will be used; rollback new import and exit early. rollback(); flushEvents(true); - return existing; + return existing.ToEntityFrameworkLive(); } LogForModel(item, @"Found existing but failed re-use check."); @@ -454,7 +448,7 @@ namespace osu.Game.Database } flushEvents(true); - return item; + return item.ToEntityFrameworkLive(); }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false); /// @@ -479,7 +473,7 @@ namespace osu.Game.Database /// /// The item to export. /// The output stream to export to. - protected virtual void ExportModelTo(TModel model, Stream outputStream) + public virtual void ExportModelTo(TModel model, Stream outputStream) { using (var archive = ZipArchive.Create()) { @@ -745,9 +739,6 @@ namespace osu.Game.Database /// Whether to perform deletion. protected virtual bool ShouldDeleteArchive(string path) => false; - /// - /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. - /// public Task ImportFromStableAsync(StableStorage stableStorage) { var storage = PrepareStableStorage(stableStorage); @@ -805,6 +796,17 @@ namespace osu.Game.Database /// An existing model which matches the criteria to skip importing, else null. protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); + public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, ModelStore.ConsumableItems.Where(m => !m.DeletePending)); + + /// + /// Performs implementation specific comparisons to determine whether a given model is present in the local store. + /// + /// The whose existence needs to be checked. + /// The usable items present in the store. + /// Whether the exists. + protected virtual bool CheckLocalAvailability(TModel model, IQueryable items) + => model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any()); + /// /// Whether import can be skipped after finding an existing import early in the process. /// Only valid when is not overridden. @@ -841,7 +843,7 @@ namespace osu.Game.Database private DbSet queryModel() => ContextFactory.Get().Set(); - protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; + public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; #region Event handling / delaying diff --git a/osu.Game/Database/EntityFrameworkLive.cs b/osu.Game/Database/EntityFrameworkLive.cs new file mode 100644 index 0000000000..1d7b53911a --- /dev/null +++ b/osu.Game/Database/EntityFrameworkLive.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 System; + +namespace osu.Game.Database +{ + public class EntityFrameworkLive : ILive where T : class + { + public EntityFrameworkLive(T item) + { + Value = item; + } + + public Guid ID => throw new InvalidOperationException(); + + public void PerformRead(Action perform) + { + perform(Value); + } + + public TReturn PerformRead(Func perform) + { + return perform(Value); + } + + public void PerformWrite(Action perform) + { + perform(Value); + } + + public T Value { get; } + } +} diff --git a/osu.Game/Database/EntityFrameworkLiveExtensions.cs b/osu.Game/Database/EntityFrameworkLiveExtensions.cs new file mode 100644 index 0000000000..cd0673675e --- /dev/null +++ b/osu.Game/Database/EntityFrameworkLiveExtensions.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Database +{ + public static class EntityFrameworkLiveExtensions + { + public static ILive ToEntityFrameworkLive(this T item) + where T : class + { + return new EntityFrameworkLive(item); + } + } +} diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index c9cd9b257a..f52dc5c8ef 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -11,6 +11,6 @@ namespace osu.Game.Database { [JsonIgnore] [PrimaryKey] - Guid ID { get; set; } + Guid ID { get; } } } diff --git a/osu.Game/Database/IHasOnlineID.cs b/osu.Game/Database/IHasOnlineID.cs new file mode 100644 index 0000000000..c55c461d2d --- /dev/null +++ b/osu.Game/Database/IHasOnlineID.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. + +#nullable enable + +namespace osu.Game.Database +{ + public interface IHasOnlineID + { + /// + /// The server-side ID representing this instance, if one exists. + /// + int? OnlineID { get; } + } +} diff --git a/osu.Game/Database/ILive.cs b/osu.Game/Database/ILive.cs new file mode 100644 index 0000000000..9359b09eaf --- /dev/null +++ b/osu.Game/Database/ILive.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Database +{ + /// + /// A wrapper to provide access to database backed classes in a thread-safe manner. + /// + /// The databased type. + public interface ILive where T : class // TODO: Add IHasGuidPrimaryKey once we don't need EF support any more. + { + Guid ID { get; } + + /// + /// Perform a read operation on this live object. + /// + /// The action to perform. + void PerformRead(Action perform); + + /// + /// Perform a read operation on this live object. + /// + /// The action to perform. + TReturn PerformRead(Func perform); + + /// + /// Perform a write operation on this live object. + /// + /// The action to perform. + void PerformWrite(Action perform); + + /// + /// Resolve the value of this instance on the current thread's context. + /// + /// + /// After resolving the data should not be passed between threads. + /// + T Value { get; } + } +} diff --git a/osu.Game/Database/IModelDownloader.cs b/osu.Game/Database/IModelDownloader.cs index 0cb633280e..a5573b2190 100644 --- a/osu.Game/Database/IModelDownloader.cs +++ b/osu.Game/Database/IModelDownloader.cs @@ -11,7 +11,7 @@ namespace osu.Game.Database /// Represents a that can download new models from an external source. /// /// The model type. - public interface IModelDownloader : IModelManager + public interface IModelDownloader : IPostNotifications where TModel : class { /// @@ -26,13 +26,6 @@ namespace osu.Game.Database /// IBindable>> DownloadFailed { get; } - /// - /// Checks whether a given is already available in the local store. - /// - /// The whose existence needs to be checked. - /// Whether the exists. - bool IsAvailableLocally(TModel model); - /// /// Begin a download for the requested . /// diff --git a/osu.Game/Database/IModelFileManager.cs b/osu.Game/Database/IModelFileManager.cs new file mode 100644 index 0000000000..c74b945eb7 --- /dev/null +++ b/osu.Game/Database/IModelFileManager.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; + +namespace osu.Game.Database +{ + public interface IModelFileManager + where TModel : class + where TFileModel : class + { + /// + /// Replace an existing file with a new version. + /// + /// The item to operate on. + /// The existing file to be replaced. + /// The new file contents. + /// An optional filename for the new file. Will use the previous filename if not specified. + void ReplaceFile(TModel model, TFileModel file, Stream contents, string filename = null); + + /// + /// Delete an existing file. + /// + /// The item to operate on. + /// The existing file to be deleted. + void DeleteFile(TModel model, TFileModel file); + + /// + /// Add a new file. + /// + /// The item to operate on. + /// The new file contents. + /// The filename for the new file. + void AddFile(TModel model, Stream contents, string filename); + } +} diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs new file mode 100644 index 0000000000..e94af01772 --- /dev/null +++ b/osu.Game/Database/IModelImporter.cs @@ -0,0 +1,65 @@ +// 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.Threading; +using System.Threading.Tasks; +using osu.Game.IO.Archives; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Database +{ + /// + /// A class which handles importing of asociated models to the game store. + /// + /// The model type. + public interface IModelImporter : IPostNotifications + where TModel : class + { + /// + /// Import one or more items from filesystem . + /// + /// + /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. + /// This will post notifications tracking progress. + /// + /// One or more archive locations on disk. + Task Import(params string[] paths); + + Task Import(params ImportTask[] tasks); + + Task>> Import(ProgressNotification notification, params ImportTask[] tasks); + + /// + /// Import one from the filesystem and delete the file on success. + /// Note that this bypasses the UI flow and should only be used for special cases or testing. + /// + /// The containing data about the to import. + /// Whether this is a low priority import. + /// An optional cancellation token. + /// The imported model, if successful. + Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Silently import an item from an . + /// + /// The archive to be imported. + /// Whether this is a low priority import. + /// An optional cancellation token. + Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Silently import an item from a . + /// + /// The model to be imported. + /// An optional archive to use for model population. + /// Whether this is a low priority import. + /// An optional cancellation token. + Task> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// A user displayable name for the model type associated with this manager. + /// + string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; + } +} diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 8c314f1617..f5e401cdfb 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -1,8 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; using osu.Framework.Bindables; +using osu.Game.IO; namespace osu.Game.Database { @@ -10,7 +14,7 @@ namespace osu.Game.Database /// Represents a model manager that publishes events when s are added or removed. /// /// The model type. - public interface IModelManager + public interface IModelManager : IModelImporter where TModel : class { /// @@ -24,5 +28,63 @@ namespace osu.Game.Database /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. /// IBindable> ItemRemoved { get; } + + /// + /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. + /// + Task ImportFromStableAsync(StableStorage stableStorage); + + /// + /// Exports an item to a legacy (.zip based) package. + /// + /// The item to export. + void Export(TModel item); + + /// + /// Exports an item to the given output stream. + /// + /// The item to export. + /// The output stream to export to. + void ExportModelTo(TModel model, Stream outputStream); + + /// + /// Perform an update of the specified item. + /// TODO: Support file additions/removals. + /// + /// The item to update. + void Update(TModel item); + + /// + /// Delete an item from the manager. + /// Is a no-op for already deleted items. + /// + /// The item to delete. + /// false if no operation was performed + bool Delete(TModel item); + + /// + /// Delete multiple items. + /// This will post notifications tracking progress. + /// + void Delete(List items, bool silent = false); + + /// + /// Restore multiple items that were previously deleted. + /// This will post notifications tracking progress. + /// + void Undelete(List items, bool silent = false); + + /// + /// Restore an item that was previously deleted. Is a no-op if the item is not in a deleted state, or has its protected flag set. + /// + /// The item to restore + void Undelete(TModel item); + + /// + /// Checks whether a given is already available in the local store. + /// + /// The whose existence needs to be checked. + /// Whether the exists. + bool IsAvailableLocally(TModel model); } } diff --git a/osu.Game/Database/INamedFileUsage.cs b/osu.Game/Database/INamedFileUsage.cs new file mode 100644 index 0000000000..e558ffe0fb --- /dev/null +++ b/osu.Game/Database/INamedFileUsage.cs @@ -0,0 +1,25 @@ +// 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.IO; + +#nullable enable + +namespace osu.Game.Database +{ + /// + /// A usage of a file, with a local filename attached. + /// + public interface INamedFileUsage + { + /// + /// The underlying file on disk. + /// + IFileInfo File { get; } + + /// + /// The filename for this usage. + /// + string Filename { get; } + } +} diff --git a/osu.Game/Database/IPostImports.cs b/osu.Game/Database/IPostImports.cs new file mode 100644 index 0000000000..f09285089a --- /dev/null +++ b/osu.Game/Database/IPostImports.cs @@ -0,0 +1,17 @@ +// 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; + +namespace osu.Game.Database +{ + public interface IPostImports + where TModel : class + { + /// + /// Fired when the user requests to view the resulting import. + /// + public Action>> PostImport { set; } + } +} diff --git a/osu.Game/Database/IPostNotifications.cs b/osu.Game/Database/IPostNotifications.cs new file mode 100644 index 0000000000..d4fd64e79e --- /dev/null +++ b/osu.Game/Database/IPostNotifications.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Database +{ + public interface IPostNotifications + { + /// + /// And action which will be fired when a notification should be presented to the user. + /// + public Action PostNotification { set; } + } +} diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 0e93e5bf4f..a957424584 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -9,20 +9,12 @@ namespace osu.Game.Database { /// /// The main realm context, bound to the update thread. - /// If querying from a non-update thread is needed, use or to receive a context instead. /// Realm Context { get; } /// - /// Get a fresh context for read usage. + /// Create a new realm context for use on the current thread. /// - RealmContextFactory.RealmUsage GetForRead(); - - /// - /// Request a context for write usage. - /// This method may block if a write is already active on a different thread. - /// - /// A usage containing a usable context. - RealmContextFactory.RealmWriteUsage GetForWrite(); + Realm CreateContext(); } } diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/ModelDownloader.cs similarity index 64% rename from osu.Game/Database/DownloadableArchiveModelManager.cs rename to osu.Game/Database/ModelDownloader.cs index da3144e8d0..e613b39b6b 100644 --- a/osu.Game/Database/DownloadableArchiveModelManager.cs +++ b/osu.Game/Database/ModelDownloader.cs @@ -1,29 +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 Humanizer; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Game.Online.API; -using osu.Game.Overlays.Notifications; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Humanizer; using osu.Framework.Bindables; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Online.API; +using osu.Game.Overlays.Notifications; namespace osu.Game.Database { - /// - /// An that has the ability to download models using an and - /// import them into the store. - /// - /// The model type. - /// The associated file join type. - public abstract class DownloadableArchiveModelManager : ArchiveModelManager, IModelDownloader - where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable - where TFileModel : class, INamedFileInfo, new() + public abstract class ModelDownloader : IModelDownloader + where TModel : class, IHasPrimaryKey, ISoftDelete, IEquatable { + public Action PostNotification { protected get; set; } + public IBindable>> DownloadBegan => downloadBegan; private readonly Bindable>> downloadBegan = new Bindable>>(); @@ -32,18 +27,15 @@ namespace osu.Game.Database private readonly Bindable>> downloadFailed = new Bindable>>(); + private readonly IModelManager modelManager; private readonly IAPIProvider api; private readonly List> currentDownloads = new List>(); - private readonly MutableDatabaseBackedStoreWithFileIncludes modelStore; - - protected DownloadableArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, IAPIProvider api, MutableDatabaseBackedStoreWithFileIncludes modelStore, - IIpcHost importHost = null) - : base(storage, contextFactory, modelStore, importHost) + protected ModelDownloader(IModelManager modelManager, IAPIProvider api, IIpcHost importHost = null) { + this.modelManager = modelManager; this.api = api; - this.modelStore = modelStore; } /// @@ -54,12 +46,6 @@ namespace osu.Game.Database /// The request object. protected abstract ArchiveDownloadRequest CreateDownloadRequest(TModel model, bool minimiseDownloadSize); - /// - /// Begin a download for the requested . - /// - /// The to be downloaded. - /// Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle. - /// Whether the download was started. public bool Download(TModel model, bool minimiseDownloadSize = false) { if (!canDownload(model)) return false; @@ -82,7 +68,7 @@ namespace osu.Game.Database Task.Factory.StartNew(async () => { // This gets scheduled back to the update thread, but we want the import to run in the background. - var imported = await Import(notification, new ImportTask(filename)).ConfigureAwait(false); + var imported = await modelManager.Import(notification, new ImportTask(filename)).ConfigureAwait(false); // for now a failed import will be marked as a failed download for simplicity. if (!imported.Any()) @@ -117,21 +103,10 @@ namespace osu.Game.Database notification.State = ProgressNotificationState.Cancelled; if (!(error is OperationCanceledException)) - Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!"); + Logger.Error(error, $"{modelManager.HumanisedModelName.Titleize()} download failed!"); } } - public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending)); - - /// - /// Performs implementation specific comparisons to determine whether a given model is present in the local store. - /// - /// The whose existence needs to be checked. - /// The usable items present in the store. - /// Whether the exists. - protected virtual bool CheckLocalAvailability(TModel model, IQueryable items) - => model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any()); - public ArchiveDownloadRequest GetExistingDownload(TModel model) => currentDownloads.Find(r => r.Model.Equals(model)); private bool canDownload(TModel model) => GetExistingDownload(model) == null && api != null; diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index ed3dc01f15..0ff902a8bc 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Development; @@ -10,80 +9,117 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; -using osu.Game.Input.Bindings; using Realms; +#nullable enable + 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 { private readonly Storage storage; - private const string database_name = @"client"; + /// + /// The filename of this realm. + /// + public readonly string Filename; private const int schema_version = 6; /// - /// Lock object which is held for the duration of a write operation (via ). + /// Lock object which is held during sections, blocking context creation during blocking periods. /// - private readonly object writeLock = new object(); + private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1); - /// - /// Lock object which is held during sections. - /// - private readonly SemaphoreSlim blockingLock = new SemaphoreSlim(1); - - private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); - private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); - private static readonly GlobalStatistic pending_writes = GlobalStatistics.Get("Realm", "Pending writes"); - private static readonly GlobalStatistic active_usages = GlobalStatistics.Get("Realm", "Active usages"); - private readonly object updateContextLock = new object(); - - private Realm context; + private readonly object contextLock = new object(); + private Realm? context; public Realm Context { get { if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException($"Use {nameof(GetForRead)} or {nameof(GetForWrite)} when performing realm operations from a non-update thread"); + throw new InvalidOperationException($"Use {nameof(CreateContext)} when performing realm operations from a non-update thread"); - lock (updateContextLock) + lock (contextLock) { if (context == null) { - context = createContext(); + context = CreateContext(); Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); } // creating a context will ensure our schema is up-to-date and migrated. - return context; } } } - public RealmContextFactory(Storage storage) + public RealmContextFactory(Storage storage, string filename) { this.storage = storage; + + Filename = filename; + + const string realm_extension = ".realm"; + + if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) + Filename += realm_extension; } - public RealmUsage GetForRead() + /// + /// Compact this realm. + /// + /// + public bool Compact() => Realm.Compact(getConfiguration()); + + protected override void Update() { - reads.Value++; - return new RealmUsage(createContext()); + base.Update(); + + lock (contextLock) + { + if (context?.Refresh() == true) + refreshes.Value++; + } } - public RealmWriteUsage GetForWrite() + public Realm CreateContext() { - writes.Value++; - pending_writes.Value++; + if (IsDisposed) + throw new ObjectDisposedException(nameof(RealmContextFactory)); - Monitor.Enter(writeLock); - return new RealmWriteUsage(createContext(), writeComplete); + try + { + contextCreationLock.Wait(); + + contexts_created.Value++; + + return Realm.GetInstance(getConfiguration()); + } + finally + { + contextCreationLock.Release(); + } + } + + private RealmConfiguration getConfiguration() + { + return new RealmConfiguration(storage.GetFullPath(Filename, true)) + { + SchemaVersion = schema_version, + MigrationCallback = onMigration, + }; + } + + private void onMigration(Migration migration, ulong lastSchemaVersion) + { } /// @@ -99,165 +135,63 @@ namespace osu.Game.Database 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."); + Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); - blockingLock.Wait(); - flushContexts(); - - return new InvokeOnDisposal(this, endBlockingSection); - - static void endBlockingSection(RealmContextFactory factory) - { - factory.blockingLock.Release(); - Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); - } - } - - protected override void Update() - { - base.Update(); - - lock (updateContextLock) - { - if (context?.Refresh() == true) - refreshes.Value++; - } - } - - private Realm createContext() - { try { - if (IsDisposed) - throw new ObjectDisposedException(nameof(RealmContextFactory)); + contextCreationLock.Wait(); - blockingLock.Wait(); - - contexts_created.Value++; - - return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) + lock (contextLock) { - SchemaVersion = schema_version, - MigrationCallback = onMigration, - }); + context?.Dispose(); + context = null; + } + + const int sleep_length = 200; + int timeout = 5000; + + // see https://github.com/realm/realm-dotnet/discussions/2657 + while (!Compact()) + { + Thread.Sleep(sleep_length); + timeout -= sleep_length; + + if (timeout < 0) + throw new TimeoutException("Took too long to acquire lock"); + } } - finally + catch { - blockingLock.Release(); + contextCreationLock.Release(); + throw; } - } - private void writeComplete() - { - Monitor.Exit(writeLock); - pending_writes.Value--; - } - - private void onMigration(Migration migration, ulong lastSchemaVersion) - { - switch (lastSchemaVersion) + return new InvokeOnDisposal(this, factory => { - case 5: - // let's keep things simple. changing the type of the primary key is a bit involved. - migration.NewRealm.RemoveAll(); - break; - } - } - - private void flushContexts() - { - Logger.Log(@"Flushing realm contexts...", LoggingTarget.Database); - Debug.Assert(blockingLock.CurrentCount == 0); - - Realm previousContext; - - lock (updateContextLock) - { - previousContext = context; - context = null; - } - - // wait for all threaded usages to finish - while (active_usages.Value > 0) - Thread.Sleep(50); - - previousContext?.Dispose(); - - Logger.Log(@"Realm contexts flushed.", LoggingTarget.Database); + factory.contextCreationLock.Release(); + Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); + }); } protected override void Dispose(bool isDisposing) { + lock (contextLock) + { + context?.Dispose(); + } + if (!IsDisposed) { - // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. - BlockAllOperations(); - blockingLock?.Dispose(); + // intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal. + contextCreationLock.Wait(); + contextCreationLock.Dispose(); } base.Dispose(isDisposing); } - - /// - /// A usage of realm from an arbitrary thread. - /// - public class RealmUsage : IDisposable - { - public readonly Realm Realm; - - internal RealmUsage(Realm context) - { - active_usages.Value++; - Realm = context; - } - - /// - /// Disposes this instance, calling the initially captured action. - /// - public virtual void Dispose() - { - Realm?.Dispose(); - active_usages.Value--; - } - } - - /// - /// A transaction used for making changes to realm data. - /// - public class RealmWriteUsage : RealmUsage - { - private readonly Action onWriteComplete; - private readonly Transaction transaction; - - internal RealmWriteUsage(Realm context, Action onWriteComplete) - : base(context) - { - this.onWriteComplete = onWriteComplete; - transaction = Realm.BeginWrite(); - } - - /// - /// Commit all changes made in this transaction. - /// - public void Commit() => transaction.Commit(); - - /// - /// Revert all changes made in this transaction. - /// - public void Rollback() => transaction.Rollback(); - - /// - /// Disposes this instance, calling the initially captured action. - /// - public override void Dispose() - { - // rollback if not explicitly committed. - transaction?.Dispose(); - - base.Dispose(); - - onWriteComplete(); - } - } } } diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index aee36e81c5..e6f3dba39f 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -1,51 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using AutoMapper; -using osu.Game.Input.Bindings; +using System; using Realms; namespace osu.Game.Database { public static class RealmExtensions { - private static readonly IMapper mapper = new MapperConfiguration(c => + public static void Write(this Realm realm, Action function) { - c.ShouldMapField = fi => false; - c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; - - c.CreateMap(); - }).CreateMapper(); - - /// - /// Create a detached copy of the each item in the collection. - /// - /// A list of managed s to detach. - /// The type of object. - /// A list containing non-managed copies of provided items. - public static List Detach(this IEnumerable items) where T : RealmObject - { - var list = new List(); - - foreach (var obj in items) - list.Add(obj.Detach()); - - return list; + using var transaction = realm.BeginWrite(); + function(realm); + transaction.Commit(); } - /// - /// Create a detached copy of the item. - /// - /// The managed to detach. - /// The type of object. - /// A non-managed copy of provided item. Will return the provided item if already detached. - public static T Detach(this T item) where T : RealmObject + public static T Write(this Realm realm, Func function) { - if (!item.IsManaged) - return item; - - return mapper.Map(item); + using var transaction = realm.BeginWrite(); + var result = function(realm); + transaction.Commit(); + return result; } } } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs new file mode 100644 index 0000000000..c5aa1399a3 --- /dev/null +++ b/osu.Game/Database/RealmObjectExtensions.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 AutoMapper; +using osu.Game.Input.Bindings; +using Realms; + +namespace osu.Game.Database +{ + public static class RealmObjectExtensions + { + private static readonly IMapper mapper = new MapperConfiguration(c => + { + c.ShouldMapField = fi => false; + c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; + + c.CreateMap(); + }).CreateMapper(); + + /// + /// Create a detached copy of the each item in the collection. + /// + /// A list of managed s to detach. + /// The type of object. + /// A list containing non-managed copies of provided items. + public static List Detach(this IEnumerable items) where T : RealmObject + { + var list = new List(); + + foreach (var obj in items) + list.Add(obj.Detach()); + + return list; + } + + /// + /// Create a detached copy of the item. + /// + /// The managed to detach. + /// The type of object. + /// A non-managed copy of provided item. Will return the provided item if already detached. + public static T Detach(this T item) where T : RealmObject + { + if (!item.IsManaged) + return item; + + return mapper.Map(item); + } + } +} diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 13c37ddfe9..ff81637efb 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -115,7 +115,7 @@ namespace osu.Game.Database createNewTask(); } - List foundUsers = request.Result?.Users; + List foundUsers = request.Response?.Users; if (foundUsers != null) { diff --git a/osu.Game/Overlays/AccountCreation/ErrorTextFlowContainer.cs b/osu.Game/Graphics/ErrorTextFlowContainer.cs similarity index 95% rename from osu.Game/Overlays/AccountCreation/ErrorTextFlowContainer.cs rename to osu.Game/Graphics/ErrorTextFlowContainer.cs index 87ff4dd398..f17a2a2c3d 100644 --- a/osu.Game/Overlays/AccountCreation/ErrorTextFlowContainer.cs +++ b/osu.Game/Graphics/ErrorTextFlowContainer.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Containers; using osuTK.Graphics; -namespace osu.Game.Overlays.AccountCreation +namespace osu.Game.Graphics { public class ErrorTextFlowContainer : OsuTextFlowContainer { diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index b6090d0e1a..edb484021c 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -21,6 +21,8 @@ namespace osu.Game.Graphics public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular); + public static FontUsage TorusAlternate => GetFont(Typeface.TorusAlternate, weight: FontWeight.Regular); + public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular); /// @@ -57,6 +59,9 @@ namespace osu.Game.Graphics case Typeface.Torus: return "Torus"; + case Typeface.TorusAlternate: + return "Torus-Alternate"; + case Typeface.Inter: return "Inter"; } @@ -113,6 +118,7 @@ namespace osu.Game.Graphics { Venera, Torus, + TorusAlternate, Inter, } diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index 244658b75e..16555075d1 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -25,7 +25,9 @@ namespace osu.Game.Graphics.UserInterface set => current.Current = value; } - private SpriteText displayedCountSpriteText; + private IHasText displayedCountText; + + public Drawable DrawableCount { get; private set; } /// /// If true, the roll-up duration will be proportional to change in value. @@ -72,16 +74,16 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load() { - displayedCountSpriteText = CreateSpriteText(); + displayedCountText = CreateText(); UpdateDisplay(); - Child = displayedCountSpriteText; + Child = DrawableCount = (Drawable)displayedCountText; } protected void UpdateDisplay() { - if (displayedCountSpriteText != null) - displayedCountSpriteText.Text = FormatCount(DisplayedCount); + if (displayedCountText != null) + displayedCountText.Text = FormatCount(DisplayedCount); } protected override void LoadComplete() @@ -160,6 +162,15 @@ namespace osu.Game.Graphics.UserInterface this.TransformTo(nameof(DisplayedCount), newValue, rollingTotalDuration, RollingEasing); } + /// + /// Creates the text. Delegates to by default. + /// + protected virtual IHasText CreateText() => CreateSpriteText(); + + /// + /// Creates an which may be used to display this counter's text. + /// May not be called if is overridden. + /// protected virtual OsuSpriteText CreateSpriteText() => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40f), diff --git a/osu.Game/IO/FileInfo.cs b/osu.Game/IO/FileInfo.cs index e04bfb46cc..331546f9f8 100644 --- a/osu.Game/IO/FileInfo.cs +++ b/osu.Game/IO/FileInfo.cs @@ -6,7 +6,7 @@ using osu.Game.Database; namespace osu.Game.IO { - public class FileInfo : IHasPrimaryKey + public class FileInfo : IHasPrimaryKey, IFileInfo { public int ID { get; set; } diff --git a/osu.Game/IO/IFileInfo.cs b/osu.Game/IO/IFileInfo.cs new file mode 100644 index 0000000000..080d8e57f5 --- /dev/null +++ b/osu.Game/IO/IFileInfo.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +namespace osu.Game.IO +{ + /// + /// A representation of a tracked file. + /// + public interface IFileInfo + { + /// + /// SHA-256 hash of the file content. + /// + string Hash { get; } + } +} diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 03cb4031ca..5fa3ccdeb9 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -7,6 +7,7 @@ using osu.Framework.Input.Bindings; using osu.Game.Database; using osu.Game.Input.Bindings; using osu.Game.Rulesets; +using Realms; #nullable enable @@ -30,9 +31,9 @@ namespace osu.Game.Input { List combinations = new List(); - using (var context = realmFactory.GetForRead()) + using (var context = realmFactory.CreateContext()) { - foreach (var action in context.Realm.All().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction)) + foreach (var action in context.All().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction)) { string str = action.KeyCombination.ReadableString(); @@ -52,26 +53,27 @@ namespace osu.Game.Input /// The rulesets to populate defaults from. public void Register(KeyBindingContainer container, IEnumerable rulesets) { - using (var usage = realmFactory.GetForWrite()) + using (var realm = realmFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) { // intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed. // this is much faster as a result. - var existingBindings = usage.Realm.All().ToList(); + var existingBindings = realm.All().ToList(); - insertDefaults(usage, existingBindings, container.DefaultKeyBindings); + insertDefaults(realm, existingBindings, container.DefaultKeyBindings); foreach (var ruleset in rulesets) { var instance = ruleset.CreateInstance(); foreach (var variant in instance.AvailableVariants) - insertDefaults(usage, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); + insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); } - usage.Commit(); + transaction.Commit(); } } - private void insertDefaults(RealmContextFactory.RealmUsage usage, List existingBindings, IEnumerable defaults, int? rulesetId = null, int? variant = null) + private void insertDefaults(Realm realm, List existingBindings, IEnumerable defaults, int? rulesetId = null, int? variant = null) { // compare counts in database vs defaults for each action type. foreach (var defaultsForAction in defaults.GroupBy(k => k.Action)) @@ -83,7 +85,7 @@ namespace osu.Game.Input continue; // insert any defaults which are missing. - usage.Realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding + realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding { KeyCombinationString = k.KeyCombination.ToString(), ActionInt = (int)k.Action, diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index af14cdc7b3..94508e3a81 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -35,9 +35,8 @@ namespace osu.Game.Online.API public string WebsiteRootUrl { get; } - /// - /// The username/email provided by the user when initiating a login. - /// + public Exception LastLoginError { get; private set; } + public string ProvidedUsername { get; private set; } private string password; @@ -136,14 +135,23 @@ namespace osu.Game.Online.API // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); - if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(ProvidedUsername, password)) + if (!authentication.HasValidAccessToken) { - //todo: this fails even on network-related issues. we should probably handle those differently. - //NotificationOverlay.ShowMessage("Login failed!"); - log.Add(@"Login failed!"); - password = null; - authentication.Clear(); - continue; + LastLoginError = null; + + try + { + authentication.AuthenticateWithLogin(ProvidedUsername, password); + } + catch (Exception e) + { + //todo: this fails even on network-related issues. we should probably handle those differently. + LastLoginError = e; + log.Add(@"Login failed!"); + password = null; + authentication.Clear(); + continue; + } } var userReq = new GetUserRequest(); diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index cf17ed4b5d..d60c9cfe65 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.IO.Network; using osu.Framework.Logging; @@ -17,7 +18,11 @@ namespace osu.Game.Online.API { protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest(Uri); - public T Result { get; private set; } + /// + /// The deserialised response object. May be null if the request or deserialisation failed. + /// + [CanBeNull] + public T Response { get; private set; } /// /// Invoked on successful completion of an API request. @@ -27,21 +32,21 @@ namespace osu.Game.Online.API protected APIRequest() { - base.Success += () => Success?.Invoke(Result); + base.Success += () => Success?.Invoke(Response); } protected override void PostProcess() { base.PostProcess(); - Result = ((OsuJsonWebRequest)WebRequest)?.ResponseObject; + Response = ((OsuJsonWebRequest)WebRequest)?.ResponseObject; } internal void TriggerSuccess(T result) { - if (Result != null) + if (Response != null) throw new InvalidOperationException("Attempted to trigger success more than once"); - Result = result; + Response = result; TriggerSuccess(); } diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 1ba31db9fa..8f91a4d198 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -32,6 +32,8 @@ namespace osu.Game.Online.API public string WebsiteRootUrl => "http://localhost"; + public Exception LastLoginError { get; private set; } + /// /// Provide handling logic for an arbitrary API request. /// Should return true is a request was handled. If null or false return, the request will be failed with a . @@ -40,6 +42,8 @@ namespace osu.Game.Online.API private readonly Bindable state = new Bindable(APIState.Online); + private bool shouldFailNextLogin; + /// /// The current connectivity state of the API. /// @@ -74,6 +78,18 @@ namespace osu.Game.Online.API public void Login(string username, string password) { + state.Value = APIState.Connecting; + + if (shouldFailNextLogin) + { + LastLoginError = new APIException("Not powerful enough to login.", new ArgumentException(nameof(shouldFailNextLogin))); + + state.Value = APIState.Offline; + shouldFailNextLogin = false; + return; + } + + LastLoginError = null; LocalUser.Value = new User { Username = username, @@ -102,5 +118,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; IBindable IAPIProvider.Activity => Activity; + + public void FailNextLogin() => shouldFailNextLogin = true; } } diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 5ad5367924..72ca37bcf4 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Game.Users; @@ -55,6 +56,11 @@ namespace osu.Game.Online.API /// string WebsiteRootUrl { get; } + /// + /// The last login error that occurred, if any. + /// + Exception? LastLoginError { get; } + /// /// The current connection state of the API. /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index bdc47aab8d..d79fc58d1c 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -1,8 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Diagnostics; using System.Net.Http; +using Newtonsoft.Json; using osu.Framework.Bindables; namespace osu.Game.Online.API @@ -32,10 +34,10 @@ namespace osu.Game.Online.API this.endpoint = endpoint; } - internal bool AuthenticateWithLogin(string username, string password) + internal void AuthenticateWithLogin(string username, string password) { - if (string.IsNullOrEmpty(username)) return false; - if (string.IsNullOrEmpty(password)) return false; + if (string.IsNullOrEmpty(username)) throw new ArgumentException("Missing username."); + if (string.IsNullOrEmpty(password)) throw new ArgumentException("Missing password."); using (var req = new AccessTokenRequestPassword(username, password) { @@ -49,13 +51,27 @@ namespace osu.Game.Online.API { req.Perform(); } - catch + catch (Exception ex) { - return false; + Token.Value = null; + + var throwableException = ex; + + try + { + // attempt to decode a displayable error string. + var error = JsonConvert.DeserializeObject(req.GetResponseString() ?? string.Empty); + if (error != null) + throwableException = new APIException(error.UserDisplayableError, ex); + } + catch + { + } + + throw throwableException; } Token.Value = req.ResponseObject; - return true; } } @@ -182,5 +198,19 @@ namespace osu.Game.Online.API base.PrePerform(); } } + + private class OAuthError + { + public string UserDisplayableError => !string.IsNullOrEmpty(Hint) ? Hint : ErrorIdentifier; + + [JsonProperty("error")] + public string ErrorIdentifier { get; set; } + + [JsonProperty("hint")] + public string Hint { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + } } } diff --git a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs index 87925b94c6..901f7365b8 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs @@ -8,13 +8,13 @@ namespace osu.Game.Online.API.Requests { public class GetBeatmapRequest : APIRequest { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; - public GetBeatmapRequest(BeatmapInfo beatmap) + public GetBeatmapRequest(BeatmapInfo beatmapInfo) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; } - protected override string Target => $@"beatmaps/lookup?id={beatmap.OnlineBeatmapID}&checksum={beatmap.MD5Hash}&filename={System.Uri.EscapeUriString(beatmap.Path ?? string.Empty)}"; + protected override string Target => $@"beatmaps/lookup?id={beatmapInfo.OnlineBeatmapID}&checksum={beatmapInfo.MD5Hash}&filename={System.Uri.EscapeUriString(beatmapInfo.Path ?? string.Empty)}"; } } diff --git a/osu.Game/Online/API/Requests/GetMessagesRequest.cs b/osu.Game/Online/API/Requests/GetMessagesRequest.cs index 36e81a9348..651f8a06c5 100644 --- a/osu.Game/Online/API/Requests/GetMessagesRequest.cs +++ b/osu.Game/Online/API/Requests/GetMessagesRequest.cs @@ -8,13 +8,13 @@ namespace osu.Game.Online.API.Requests { public class GetMessagesRequest : APIRequest> { - private readonly Channel channel; + public readonly Channel Channel; public GetMessagesRequest(Channel channel) { - this.channel = channel; + Channel = channel; } - protected override string Target => $@"chat/channels/{channel.Id}/messages"; + protected override string Target => $@"chat/channels/{Channel.Id}/messages"; } } diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs index b4e0e44b2c..f3bf690ed5 100644 --- a/osu.Game/Online/API/Requests/GetScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs @@ -15,20 +15,20 @@ namespace osu.Game.Online.API.Requests { public class GetScoresRequest : APIRequest { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private readonly BeatmapLeaderboardScope scope; private readonly RulesetInfo ruleset; private readonly IEnumerable mods; - public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable mods = null) + public GetScoresRequest(BeatmapInfo beatmapInfo, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable mods = null) { - if (!beatmap.OnlineBeatmapID.HasValue) + if (!beatmapInfo.OnlineBeatmapID.HasValue) throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(BeatmapInfo.OnlineBeatmapID)}."); if (scope == BeatmapLeaderboardScope.Local) throw new InvalidOperationException("Should not attempt to request online scores for a local scoped leaderboard"); - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; this.scope = scope; this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset)); this.mods = mods ?? Array.Empty(); @@ -42,7 +42,7 @@ namespace osu.Game.Online.API.Requests foreach (APILegacyScoreInfo score in r.Scores) { - score.Beatmap = beatmap; + score.BeatmapInfo = beatmapInfo; score.OnlineRulesetID = ruleset.ID.Value; } @@ -50,12 +50,12 @@ namespace osu.Game.Online.API.Requests if (userScore != null) { - userScore.Score.Beatmap = beatmap; + userScore.Score.BeatmapInfo = beatmapInfo; userScore.Score.OnlineRulesetID = ruleset.ID.Value; } } - protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}/scores{createQueryParameters()}"; + protected override string Target => $@"beatmaps/{beatmapInfo.OnlineBeatmapID}/scores{createQueryParameters()}"; private string createQueryParameters() { diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 7343870dbc..c2a68c8ca1 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -64,7 +64,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"max_combo")] private int? maxCombo { get; set; } - public virtual BeatmapInfo ToBeatmap(RulesetStore rulesets) + public virtual BeatmapInfo ToBeatmapInfo(RulesetStore rulesets) { var set = BeatmapSet?.ToBeatmapSet(rulesets); diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index f653a654ca..35963792d0 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -116,7 +116,7 @@ namespace osu.Game.Online.API.Requests.Responses beatmapSet.Beatmaps = beatmaps?.Select(b => { - var beatmap = b.ToBeatmap(rulesets); + var beatmap = b.ToBeatmapInfo(rulesets); beatmap.BeatmapSet = beatmapSet; beatmap.Metadata = beatmapSet.Metadata; return beatmap; diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs index 567df524b1..aaf2dccc82 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs @@ -37,7 +37,7 @@ namespace osu.Game.Online.API.Requests.Responses OnlineScoreID = OnlineScoreID, Date = Date, PP = PP, - Beatmap = Beatmap, + BeatmapInfo = BeatmapInfo, RulesetID = OnlineRulesetID, Hash = Replay ? "online" : string.Empty, // todo: temporary? Rank = Rank, @@ -100,7 +100,7 @@ namespace osu.Game.Online.API.Requests.Responses public DateTimeOffset Date { get; set; } [JsonProperty(@"beatmap")] - public BeatmapInfo Beatmap { get; set; } + public BeatmapInfo BeatmapInfo { get; set; } [JsonProperty("accuracy")] public double Accuracy { get; set; } @@ -114,10 +114,10 @@ namespace osu.Game.Online.API.Requests.Responses set { // extract the set ID to its correct place. - Beatmap.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = value.ID }; + BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = value.ID }; value.ID = 0; - Beatmap.Metadata = value; + BeatmapInfo.Metadata = value; } } diff --git a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs index 4614fe29b7..10f7ca6fe2 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs @@ -15,8 +15,8 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("count")] public int PlayCount { get; set; } - [JsonProperty] - private BeatmapInfo beatmap { get; set; } + [JsonProperty("beatmap")] + private BeatmapInfo beatmapInfo { get; set; } [JsonProperty] private APIBeatmapSet beatmapSet { get; set; } @@ -24,9 +24,9 @@ namespace osu.Game.Online.API.Requests.Responses public BeatmapInfo GetBeatmapInfo(RulesetStore rulesets) { BeatmapSetInfo setInfo = beatmapSet.ToBeatmapSet(rulesets); - beatmap.BeatmapSet = setInfo; - beatmap.Metadata = setInfo.Metadata; - return beatmap; + beatmapInfo.BeatmapSet = setInfo; + beatmapInfo.Metadata = setInfo.Metadata; + return beatmapInfo; } } } diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index 97a2fbdd5c..89eb00a45a 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -37,27 +37,27 @@ namespace osu.Game.Online.Chat base.LoadComplete(); string verb; - BeatmapInfo beatmap; + BeatmapInfo beatmapInfo; switch (api.Activity.Value) { case UserActivity.InGame game: verb = "playing"; - beatmap = game.Beatmap; + beatmapInfo = game.BeatmapInfo; break; case UserActivity.Editing edit: verb = "editing"; - beatmap = edit.Beatmap; + beatmapInfo = edit.BeatmapInfo; break; default: verb = "listening to"; - beatmap = currentBeatmap.Value.BeatmapInfo; + beatmapInfo = currentBeatmap.Value.BeatmapInfo; break; } - var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString(); + var beatmapString = beatmapInfo.OnlineBeatmapID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmapInfo.OnlineBeatmapID} {beatmapInfo}]" : beatmapInfo.ToString(); channelManager.PostMessage($"is {verb} {beatmapString}", true, target); Expire(); diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index d9599481e7..2a96051427 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -16,7 +16,7 @@ namespace osu.Game.Online /// public abstract class DownloadTrackingComposite : CompositeDrawable where TModel : class, IEquatable - where TModelManager : class, IModelDownloader + where TModelManager : class, IModelDownloader, IModelManager { protected readonly Bindable Model = new Bindable(); @@ -35,7 +35,7 @@ namespace osu.Game.Online Model.Value = model; } - private IBindable> managedUpdated; + private IBindable> managerUpdated; private IBindable> managerRemoved; private IBindable>> managerDownloadBegan; private IBindable>> managerDownloadFailed; @@ -60,8 +60,8 @@ namespace osu.Game.Online managerDownloadBegan.BindValueChanged(downloadBegan); managerDownloadFailed = Manager.DownloadFailed.GetBoundCopy(); managerDownloadFailed.BindValueChanged(downloadFailed); - managedUpdated = Manager.ItemUpdated.GetBoundCopy(); - managedUpdated.BindValueChanged(itemUpdated); + managerUpdated = Manager.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(itemUpdated); managerRemoved = Manager.ItemRemoved.GetBoundCopy(); managerRemoved.BindValueChanged(itemRemoved); } @@ -77,7 +77,7 @@ namespace osu.Game.Online /// /// Whether the given model is available in the database. - /// By default, this calls , + /// By default, this calls , /// but can be overriden to add additional checks for verifying the model in database. /// protected virtual bool IsModelAvailableLocally() => Manager?.IsAvailableLocally(Model.Value) == true; diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 4f8b27602b..e3ac9f603d 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -255,6 +255,7 @@ namespace osu.Game.Online.Leaderboards } private APIRequest getScoresRequest; + private ScheduledDelegate getScoresRequestCallback; protected abstract bool IsOnlineScope { get; } @@ -282,13 +283,16 @@ namespace osu.Game.Online.Leaderboards getScoresRequest?.Cancel(); getScoresRequest = null; + getScoresRequestCallback?.Cancel(); + getScoresRequestCallback = null; + pendingUpdateScores?.Cancel(); pendingUpdateScores = Schedule(() => { PlaceholderState = PlaceholderState.Retrieving; loading.Show(); - getScoresRequest = FetchScores(scores => Schedule(() => + getScoresRequest = FetchScores(scores => getScoresRequestCallback = Schedule(() => { Scores = scores.ToArray(); PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; @@ -297,7 +301,7 @@ namespace osu.Game.Online.Leaderboards if (getScoresRequest == null) return; - getScoresRequest.Failure += e => Schedule(() => + getScoresRequest.Failure += e => getScoresRequestCallback = Schedule(() => { if (e is OperationCanceledException) return; diff --git a/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs b/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs index 973dccd528..00623282d3 100644 --- a/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs +++ b/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs @@ -13,9 +13,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("checksum")] public string Checksum { get; set; } - public override BeatmapInfo ToBeatmap(RulesetStore rulesets) + public override BeatmapInfo ToBeatmapInfo(RulesetStore rulesets) { - var b = base.ToBeatmap(rulesets); + var b = base.ToBeatmapInfo(rulesets); b.MD5Hash = Checksum; return b; } diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index 30c1d2f826..7ec34e70d5 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online.Rooms OnlineScoreID = ID, TotalScore = TotalScore, MaxCombo = MaxCombo, - Beatmap = playlistItem.Beatmap.Value, + BeatmapInfo = playlistItem.Beatmap.Value, BeatmapInfoID = playlistItem.BeatmapID, Ruleset = playlistItem.Ruleset.Value, RulesetID = playlistItem.RulesetID, diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 1d409d4b56..48f1347fa1 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online.Rooms public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets) { - Beatmap.Value ??= apiBeatmap.ToBeatmap(rulesets); + Beatmap.Value ??= apiBeatmap.ToBeatmapInfo(rulesets); Ruleset.Value ??= rulesets.GetRuleset(RulesetID); Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 8c617784b9..b597b2f214 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -134,7 +134,7 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } - public void BeginPlaying(GameplayBeatmap beatmap, Score score) + public void BeginPlaying(GameplayState state, Score score) { Debug.Assert(ThreadSafety.IsUpdateThread); @@ -144,11 +144,11 @@ namespace osu.Game.Online.Spectator IsPlaying = true; // transfer state at point of beginning play - currentState.BeatmapID = score.ScoreInfo.Beatmap.OnlineBeatmapID; + currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineBeatmapID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); - currentBeatmap = beatmap.PlayableBeatmap; + currentBeatmap = state.Beatmap; currentScore = score; BeginPlayingInternal(currentState); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 99925bb1fb..8a018f17d9 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -482,7 +482,7 @@ namespace osu.Game return; } - var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.ID == databasedScoreInfo.Beatmap.ID); + var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.ID == databasedScoreInfo.BeatmapInfo.ID); if (databasedBeatmap == null) { @@ -624,10 +624,10 @@ namespace osu.Game SkinManager.PostNotification = n => Notifications.Post(n); BeatmapManager.PostNotification = n => Notifications.Post(n); - BeatmapManager.PresentImport = items => PresentBeatmap(items.First()); + BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value); ScoreManager.PostNotification = n => Notifications.Post(n); - ScoreManager.PresentImport = items => PresentScore(items.First()); + ScoreManager.PostImport = items => PresentScore(items.First().Value); // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7aa460981a..7f4fe8a943 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -13,24 +13,23 @@ using osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.IO.Stores; -using osu.Framework.Platform; -using osu.Game.Beatmaps; -using osu.Game.Configuration; -using osu.Game.Graphics; -using osu.Game.Graphics.Cursor; -using osu.Game.Online.API; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Input; +using osu.Framework.IO.Stores; using osu.Framework.Logging; -using osu.Framework.Threading; +using osu.Framework.Platform; using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Cursor; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; @@ -160,8 +159,6 @@ namespace osu.Game private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(GLOBAL_TRACK_VOLUME_ADJUST); - private IBindable updateThreadState; - public OsuGameBase() { UseDevelopmentServer = DebugUtils.IsDebugBuild; @@ -187,10 +184,7 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); - - updateThreadState = Host.UpdateThread.State.GetBoundCopy(); - updateThreadState.BindValueChanged(updateThreadStateChanged); + dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client")); AddInternal(realmFactory); @@ -242,7 +236,7 @@ namespace osu.Game // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); // this should likely be moved to ArchiveModelManager when another case appears where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to @@ -250,7 +244,7 @@ namespace osu.Game List getBeatmapScores(BeatmapSetInfo set) { var beatmapIds = BeatmapManager.QueryBeatmaps(b => b.BeatmapSetInfoID == set.ID).Select(b => b.ID).ToList(); - return ScoreManager.QueryScores(s => beatmapIds.Contains(s.Beatmap.ID)).ToList(); + return ScoreManager.QueryScores(s => beatmapIds.Contains(s.BeatmapInfo.ID)).ToList(); } BeatmapManager.ItemRemoved.BindValueChanged(i => @@ -347,6 +341,11 @@ namespace osu.Game AddFont(Resources, @"Fonts/Torus/Torus-SemiBold"); AddFont(Resources, @"Fonts/Torus/Torus-Bold"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Regular"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Light"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-SemiBold"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Bold"); + AddFont(Resources, @"Fonts/Inter/Inter-Regular"); AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic"); AddFont(Resources, @"Fonts/Inter/Inter-Light"); @@ -367,23 +366,6 @@ namespace osu.Game AddFont(Resources, @"Fonts/Venera/Venera-Black"); } - private IDisposable blocking; - - private void updateThreadStateChanged(ValueChangedEvent state) - { - switch (state.NewValue) - { - case GameThreadState.Running: - blocking?.Dispose(); - blocking = null; - break; - - case GameThreadState.Paused: - blocking = realmFactory.BlockAllOperations(); - break; - } - } - protected override void LoadComplete() { base.LoadComplete(); @@ -448,19 +430,20 @@ namespace osu.Game private void migrateDataToRealm() { using (var db = contextFactory.GetForWrite()) - using (var usage = realmFactory.GetForWrite()) + using (var realm = realmFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) { // migrate ruleset settings. can be removed 20220315. var existingSettings = db.Context.DatabasedSetting; // only migrate data if the realm database is empty. - if (!usage.Realm.All().Any()) + if (!realm.All().Any()) { foreach (var dkb in existingSettings) { if (dkb.RulesetID == null) continue; - usage.Realm.Add(new RealmRulesetSetting + realm.Add(new RealmRulesetSetting { Key = dkb.Key, Value = dkb.StringValue, @@ -472,7 +455,7 @@ namespace osu.Game db.Context.RemoveRange(existingSettings); - usage.Commit(); + transaction.Commit(); } } diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index 5a6cde8229..683f4f0c49 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -38,16 +38,16 @@ namespace osu.Game.Overlays.BeatmapSet } } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; updateDisplay(); } @@ -57,7 +57,7 @@ namespace osu.Game.Overlays.BeatmapSet { bpm.Value = BeatmapSet?.OnlineInfo?.BPM.ToLocalisableString(@"0.##") ?? (LocalisableString)"-"; - if (beatmap == null) + if (beatmapInfo == null) { length.Value = string.Empty; circleCount.Value = string.Empty; @@ -65,11 +65,11 @@ namespace osu.Game.Overlays.BeatmapSet } else { - length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(beatmap.Length).ToFormattedDuration()); - length.Value = TimeSpan.FromMilliseconds(beatmap.Length).ToFormattedDuration(); + length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration()); + length.Value = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration(); - circleCount.Value = beatmap.OnlineInfo.CircleCount.ToLocalisableString(@"N0"); - sliderCount.Value = beatmap.OnlineInfo.SliderCount.ToLocalisableString(@"N0"); + circleCount.Value = beatmapInfo.OnlineInfo.CircleCount.ToLocalisableString(@"N0"); + sliderCount.Value = beatmapInfo.OnlineInfo.SliderCount.ToLocalisableString(@"N0"); } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 60e341d2ac..3df275c6d3 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -178,21 +178,21 @@ namespace osu.Game.Overlays.BeatmapSet } starRatingContainer.FadeOut(100); - Beatmap.Value = Difficulties.FirstOrDefault()?.Beatmap; + Beatmap.Value = Difficulties.FirstOrDefault()?.BeatmapInfo; plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0; favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0; updateDifficultyButtons(); } - private void showBeatmap(BeatmapInfo beatmap) + private void showBeatmap(BeatmapInfo beatmapInfo) { - version.Text = beatmap?.Version; + version.Text = beatmapInfo?.Version; } private void updateDifficultyButtons() { - Difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected); + Difficulties.Children.ToList().ForEach(diff => diff.State = diff.BeatmapInfo == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected); } public class DifficultiesContainer : FillFlowContainer @@ -216,7 +216,7 @@ namespace osu.Game.Overlays.BeatmapSet private readonly Box backgroundBox; private readonly DifficultyIcon icon; - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public Action OnHovered; public Action OnClicked; @@ -241,9 +241,9 @@ namespace osu.Game.Overlays.BeatmapSet } } - public DifficultySelectorButton(BeatmapInfo beatmap) + public DifficultySelectorButton(BeatmapInfo beatmapInfo) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; Size = new Vector2(size); Margin = new MarginPadding { Horizontal = tile_spacing / 2 }; @@ -260,7 +260,7 @@ namespace osu.Game.Overlays.BeatmapSet Alpha = 0.5f } }, - icon = new DifficultyIcon(beatmap, shouldShowTooltip: false) + icon = new DifficultyIcon(beatmapInfo, shouldShowTooltip: false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -273,7 +273,7 @@ namespace osu.Game.Overlays.BeatmapSet protected override bool OnHover(HoverEvent e) { fadeIn(); - OnHovered?.Invoke(Beatmap); + OnHovered?.Invoke(BeatmapInfo); return base.OnHover(e); } @@ -286,7 +286,7 @@ namespace osu.Game.Overlays.BeatmapSet protected override bool OnClick(ClickEvent e) { - OnClicked?.Invoke(Beatmap); + OnClicked?.Invoke(BeatmapInfo); return base.OnClick(e); } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index c3b6444a24..dcf06ac7fb 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -211,7 +211,7 @@ namespace osu.Game.Overlays.BeatmapSet Picker.Beatmap.ValueChanged += b => { - Details.Beatmap = b.NewValue; + Details.BeatmapInfo = b.NewValue; externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; }; } diff --git a/osu.Game/Overlays/BeatmapSet/Details.cs b/osu.Game/Overlays/BeatmapSet/Details.cs index 680487ffbb..92361ae4f8 100644 --- a/osu.Game/Overlays/BeatmapSet/Details.cs +++ b/osu.Game/Overlays/BeatmapSet/Details.cs @@ -37,16 +37,16 @@ namespace osu.Game.Overlays.BeatmapSet } } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - basic.Beatmap = advanced.Beatmap = beatmap = value; + basic.BeatmapInfo = advanced.BeatmapInfo = beatmapInfo = value; } } diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index f9b8de9dba..61c660cbaa 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -24,10 +24,10 @@ namespace osu.Game.Overlays.BeatmapSet public readonly Bindable BeatmapSet = new Bindable(); - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => successRate.Beatmap; - set => successRate.Beatmap = value; + get => successRate.BeatmapInfo; + set => successRate.BeatmapInfo = value; } public Info() diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 8fe1d35b62..018faf2011 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -172,7 +172,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Text = score.MaxCombo.ToLocalisableString(@"0\x"), Font = OsuFont.GetFont(size: text_size), - Colour = score.MaxCombo == score.Beatmap?.MaxCombo ? highAccuracyColour : Color4.White + Colour = score.MaxCombo == score.BeatmapInfo?.MaxCombo ? highAccuracyColour : Color4.White } }; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index fb1769fbe1..82657afc86 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -74,7 +74,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores var topScore = ordered.Result.First(); - scoreTable.DisplayScores(ordered.Result, topScore.Beatmap?.Status.GrantsPerformancePoints() == true); + scoreTable.DisplayScores(ordered.Result, topScore.BeatmapInfo?.Status.GrantsPerformancePoints() == true); scoreTable.Show(); var userScore = value.UserScore; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 883e83ce6e..630aa8fe53 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x"); - ppColumn.Alpha = value.Beatmap?.Status.GrantsPerformancePoints() == true ? 1 : 0; + ppColumn.Alpha = value.BeatmapInfo?.Status.GrantsPerformancePoints() == true ? 1 : 0; ppColumn.Text = value.PP?.ToLocalisableString(@"N0"); statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn); diff --git a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs b/osu.Game/Overlays/BeatmapSet/SuccessRate.cs index cde4589c98..4a9b8244a5 100644 --- a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs +++ b/osu.Game/Overlays/BeatmapSet/SuccessRate.cs @@ -23,16 +23,16 @@ namespace osu.Game.Overlays.BeatmapSet private readonly Bar successRate; private readonly Container percentContainer; - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; updateDisplay(); } @@ -40,15 +40,15 @@ namespace osu.Game.Overlays.BeatmapSet private void updateDisplay() { - int passCount = beatmap?.OnlineInfo?.PassCount ?? 0; - int playCount = beatmap?.OnlineInfo?.PlayCount ?? 0; + int passCount = beatmapInfo?.OnlineInfo?.PassCount ?? 0; + int playCount = beatmapInfo?.OnlineInfo?.PlayCount ?? 0; var rate = playCount != 0 ? (float)passCount / playCount : 0; successPercent.Text = rate.ToLocalisableString(@"0.#%"); successRate.Length = rate; percentContainer.ResizeWidthTo(successRate.Length, 250, Easing.InOutCubic); - Graph.Metrics = beatmap?.Metrics; + Graph.Metrics = beatmapInfo?.Metrics; } public SuccessRate() diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index bdb3715e73..f987b57d6e 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -61,7 +61,7 @@ namespace osu.Game.Overlays Header.HeaderContent.Picker.Beatmap.ValueChanged += b => { - info.Beatmap = b.NewValue; + info.BeatmapInfo = b.NewValue; ScrollFlow.ScrollToStart(); }; } diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 25c5154d4a..4b27335c7c 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -284,6 +284,10 @@ namespace osu.Game.Overlays if (currentChannel.Value != e.NewValue) return; + // check once more to ensure the channel hasn't since been removed from the loaded channels list (may have been left by some automated means). + if (!loadedChannels.Contains(loaded)) + return; + loading.Hide(); currentChannelContainer.Clear(false); @@ -444,10 +448,9 @@ namespace osu.Game.Overlays if (loaded != null) { - loadedChannels.Remove(loaded); - // Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared // to ensure that the previous channel doesn't get updated after it's disposed + loadedChannels.Remove(loaded); currentChannelContainer.Remove(loaded); loaded.Dispose(); } diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index e43b84d52a..f7842dcd30 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -42,6 +43,9 @@ namespace osu.Game.Overlays.Login Spacing = new Vector2(0, 5); AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; + + ErrorTextFlowContainer errorText; + Children = new Drawable[] { username = new OsuTextBox @@ -57,6 +61,11 @@ namespace osu.Game.Overlays.Login RelativeSizeAxes = Axes.X, TabbableContentContainer = this, }, + errorText = new ErrorTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, new SettingsCheckbox { LabelText = "Remember username", @@ -97,6 +106,9 @@ namespace osu.Game.Overlays.Login }; password.OnCommit += (sender, newText) => performLogin(); + + if (api?.LastLoginError?.Message is string error) + errorText.AddErrors(new[] { error }); } public override bool AcceptsFocus => true; @@ -108,4 +120,4 @@ namespace osu.Game.Overlays.Login Schedule(() => { GetContainingInputManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 571b14428e..ef25de77c6 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Music { Padding = new MarginPadding { Left = 5 }; - FilterTerms = item.Metadata.SearchableTerms; + FilterTerms = item.Metadata.GetSearchableTerms(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs index a8a4cfc365..7812a81f30 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs @@ -15,12 +15,12 @@ namespace osu.Game.Overlays.Profile.Sections /// public abstract class BeatmapMetadataContainer : OsuHoverContainer { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; - protected BeatmapMetadataContainer(BeatmapInfo beatmap) + protected BeatmapMetadataContainer(BeatmapInfo beatmapInfo) : base(HoverSampleSet.Submit) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; AutoSizeAxes = Axes.Both; } @@ -30,19 +30,19 @@ namespace osu.Game.Overlays.Profile.Sections { Action = () => { - if (beatmap.OnlineBeatmapID != null) - beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value); - else if (beatmap.BeatmapSet?.OnlineBeatmapSetID != null) - beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmap.BeatmapSet.OnlineBeatmapSetID.Value); + if (beatmapInfo.OnlineBeatmapID != null) + beatmapSetOverlay?.FetchAndShowBeatmap(beatmapInfo.OnlineBeatmapID.Value); + else if (beatmapInfo.BeatmapSet?.OnlineBeatmapSetID != null) + beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapInfo.BeatmapSet.OnlineBeatmapSetID.Value); }; Child = new FillFlowContainer { AutoSizeAxes = Axes.Both, - Children = CreateText(beatmap), + Children = CreateText(beatmapInfo), }; } - protected abstract Drawable[] CreateText(BeatmapInfo beatmap); + protected abstract Drawable[] CreateText(BeatmapInfo beatmapInfo); } } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index a419bef233..2c6fa76ca4 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -22,12 +22,12 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private const int cover_width = 100; private const int corner_radius = 6; - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private readonly int playCount; - public DrawableMostPlayedBeatmap(BeatmapInfo beatmap, int playCount) + public DrawableMostPlayedBeatmap(BeatmapInfo beatmapInfo, int playCount) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; this.playCount = playCount; RelativeSizeAxes = Axes.X; @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { RelativeSizeAxes = Axes.Y, Width = cover_width, - BeatmapSet = beatmap.BeatmapSet, + BeatmapSet = beatmapInfo.BeatmapSet, }, new Container { @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Direction = FillDirection.Vertical, Children = new Drawable[] { - new MostPlayedBeatmapMetadataContainer(beatmap), + new MostPlayedBeatmapMetadataContainer(beatmapInfo), new LinkFlowContainer(t => { t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); @@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical }.With(d => { d.AddText("mapped by "); - d.AddUserLink(beatmap.Metadata.Author); + d.AddUserLink(beatmapInfo.Metadata.Author); }), } }, @@ -120,23 +120,23 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private class MostPlayedBeatmapMetadataContainer : BeatmapMetadataContainer { - public MostPlayedBeatmapMetadataContainer(BeatmapInfo beatmap) - : base(beatmap) + public MostPlayedBeatmapMetadataContainer(BeatmapInfo beatmapInfo) + : base(beatmapInfo) { } - protected override Drawable[] CreateText(BeatmapInfo beatmap) => new Drawable[] + protected override Drawable[] CreateText(BeatmapInfo beatmapInfo) => new Drawable[] { new OsuSpriteText { Text = new RomanisableString( - $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] ", - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] "), + $"{beatmapInfo.Metadata.TitleUnicode ?? beatmapInfo.Metadata.Title} [{beatmapInfo.Version}] ", + $"{beatmapInfo.Metadata.Title ?? beatmapInfo.Metadata.TitleUnicode} [{beatmapInfo.Version}] "), Font = OsuFont.GetFont(weight: FontWeight.Bold) }, new OsuSpriteText { - Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), + Text = "by " + new RomanisableString(beatmapInfo.Metadata.ArtistUnicode, beatmapInfo.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.Regular) }, }; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 713303285a..3561e9700e 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks Spacing = new Vector2(0, 2), Children = new Drawable[] { - new ScoreBeatmapMetadataContainer(Score.Beatmap), + new ScoreBeatmapMetadataContainer(Score.BeatmapInfo), new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { new OsuSpriteText { - Text = $"{Score.Beatmap.Version}", + Text = $"{Score.BeatmapInfo.Version}", Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), Colour = colours.Yellow }, @@ -245,27 +245,27 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks private class ScoreBeatmapMetadataContainer : BeatmapMetadataContainer { - public ScoreBeatmapMetadataContainer(BeatmapInfo beatmap) - : base(beatmap) + public ScoreBeatmapMetadataContainer(BeatmapInfo beatmapInfo) + : base(beatmapInfo) { } - protected override Drawable[] CreateText(BeatmapInfo beatmap) => new Drawable[] + protected override Drawable[] CreateText(BeatmapInfo beatmapInfo) => new Drawable[] { new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Text = new RomanisableString( - $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} ", - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} "), + $"{beatmapInfo.Metadata.TitleUnicode ?? beatmapInfo.Metadata.Title} ", + $"{beatmapInfo.Metadata.Title ?? beatmapInfo.Metadata.TitleUnicode} "), Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true) }, new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), + Text = "by " + new RomanisableString(beatmapInfo.Metadata.ArtistUnicode, beatmapInfo.Metadata.Artist), Font = OsuFont.GetFont(size: 12, italics: true) }, }; diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index b8bdef925e..80ce2e038d 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -143,19 +143,27 @@ namespace osu.Game.Overlays switch (request) { case GetUserRankingsRequest userRequest: + if (userRequest.Response == null) + return null; + switch (userRequest.Type) { case UserRankingsType.Performance: - return new PerformanceTable(1, userRequest.Result.Users); + return new PerformanceTable(1, userRequest.Response.Users); case UserRankingsType.Score: - return new ScoresTable(1, userRequest.Result.Users); + return new ScoresTable(1, userRequest.Response.Users); } return null; case GetCountryRankingsRequest countryRequest: - return new CountriesTable(1, countryRequest.Result.Countries); + { + if (countryRequest.Response == null) + return null; + + return new CountriesTable(1, countryRequest.Response.Countries); + } } return null; diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 85d88c96f8..cf8adf2785 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -368,12 +368,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void updateStoreFromButton(KeyButton button) { - using (var usage = realmFactory.GetForWrite()) + using (var realm = realmFactory.CreateContext()) { - var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); - binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; - - usage.Commit(); + var binding = realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); + realm.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString); } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index fae0318359..2cc2857e9b 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -38,8 +38,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input List bindings; - using (var usage = realmFactory.GetForRead()) - bindings = usage.Realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + using (var realm = realmFactory.CreateContext()) + bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { @@ -75,5 +75,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input Content.CornerRadius = 5; } + + // Empty FilterTerms so that the ResetButton is visible only when the whole subsection is visible. + public override IEnumerable FilterTerms => Enumerable.Empty(); } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 224c9178ae..1143549f7f 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -7,6 +7,8 @@ using System.Linq; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; @@ -16,6 +18,14 @@ namespace osu.Game.Rulesets.Difficulty { public abstract class DifficultyCalculator { + /// + /// The beatmap for which difficulty will be calculated. + /// + protected IBeatmap Beatmap { get; private set; } + + private Mod[] playableMods; + private double clockRate; + private readonly Ruleset ruleset; private readonly WorkingBeatmap beatmap; @@ -32,14 +42,45 @@ namespace osu.Game.Rulesets.Difficulty /// A structure describing the difficulty of the beatmap. public DifficultyAttributes Calculate(params Mod[] mods) { - mods = mods.Select(m => m.DeepClone()).ToArray(); + preProcess(mods); - IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); + var skills = CreateSkills(Beatmap, playableMods, clockRate); - var track = new TrackVirtual(10000); - mods.OfType().ForEach(m => m.ApplyToTrack(track)); + if (!Beatmap.HitObjects.Any()) + return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate); - return calculate(playableBeatmap, mods, track.Rate); + foreach (var hitObject in getDifficultyHitObjects()) + { + foreach (var skill in skills) + skill.ProcessInternal(hitObject); + } + + return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate); + } + + public List CalculateTimed(params Mod[] mods) + { + preProcess(mods); + + var attribs = new List(); + + if (!Beatmap.HitObjects.Any()) + return attribs; + + var skills = CreateSkills(Beatmap, playableMods, clockRate); + var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); + + foreach (var hitObject in getDifficultyHitObjects()) + { + progressiveBeatmap.HitObjects.Add(hitObject.BaseObject); + + foreach (var skill in skills) + skill.ProcessInternal(hitObject); + + attribs.Add(new TimedDifficultyAttributes(hitObject.EndTime, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate))); + } + + return attribs; } /// @@ -57,24 +98,24 @@ namespace osu.Game.Rulesets.Difficulty } } - private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate) + /// + /// Retrieves the s to calculate against. + /// + private IEnumerable getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, clockRate)); + + /// + /// Performs required tasks before every calculation. + /// + /// The original list of s. + private void preProcess(Mod[] mods) { - var skills = CreateSkills(beatmap, mods, clockRate); + playableMods = mods.Select(m => m.DeepClone()).ToArray(); - if (!beatmap.HitObjects.Any()) - return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); + Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, playableMods); - var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList(); - - foreach (var hitObject in difficultyHitObjects) - { - foreach (var skill in skills) - { - skill.ProcessInternal(hitObject); - } - } - - return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); + var track = new TrackVirtual(10000); + playableMods.OfType().ForEach(m => m.ApplyToTrack(track)); + clockRate = track.Rate; } /// @@ -86,7 +127,7 @@ namespace osu.Game.Rulesets.Difficulty => input.OrderBy(h => h.BaseObject.StartTime); /// - /// Creates all combinations which adjust the difficulty. + /// Creates all combinations which adjust the difficulty. /// public Mod[] CreateDifficultyAdjustmentModCombinations() { @@ -154,14 +195,15 @@ namespace osu.Game.Rulesets.Difficulty } /// - /// Retrieves all s which adjust the difficulty. + /// Retrieves all s which adjust the difficulty. /// protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty(); /// /// Creates to describe beatmap's calculated difficulty. /// - /// The whose difficulty was calculated. + /// The whose difficulty was calculated. + /// This may differ from in the case of timed calculation. /// The s that difficulty was calculated with. /// The skills which processed the beatmap. /// The rate at which the gameplay clock is run at. @@ -178,10 +220,51 @@ namespace osu.Game.Rulesets.Difficulty /// /// Creates the s to calculate the difficulty of an . /// - /// The whose difficulty will be calculated. + /// The whose difficulty will be calculated. + /// This may differ from in the case of timed calculation. /// Mods to calculate difficulty with. /// Clockrate to calculate difficulty with. /// The s. protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate); + + /// + /// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time. + /// + private class ProgressiveCalculationBeatmap : IBeatmap + { + private readonly IBeatmap baseBeatmap; + + public ProgressiveCalculationBeatmap(IBeatmap baseBeatmap) + { + this.baseBeatmap = baseBeatmap; + } + + public readonly List HitObjects = new List(); + + IReadOnlyList IBeatmap.HitObjects => HitObjects; + + #region Delegated IBeatmap implementation + + public BeatmapInfo BeatmapInfo + { + get => baseBeatmap.BeatmapInfo; + set => baseBeatmap.BeatmapInfo = value; + } + + public ControlPointInfo ControlPointInfo + { + get => baseBeatmap.ControlPointInfo; + set => baseBeatmap.ControlPointInfo = value; + } + + public BeatmapMetadata Metadata => baseBeatmap.Metadata; + public List Breaks => baseBeatmap.Breaks; + public double TotalBreakTime => baseBeatmap.TotalBreakTime; + public IEnumerable GetStatistics() => baseBeatmap.GetStatistics(); + public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength(); + public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone()); + + #endregion + } } } diff --git a/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs new file mode 100644 index 0000000000..973b2dacb2 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Rulesets.Difficulty +{ + /// + /// Wraps a object and adds a time value for which the attribute is valid. + /// Output by . + /// + public class TimedDifficultyAttributes : IComparable + { + public readonly double Time; + public readonly DifficultyAttributes Attributes; + + public TimedDifficultyAttributes(double time, DifficultyAttributes attributes) + { + Time = time; + Attributes = attributes; + } + + public int CompareTo(TimedDifficultyAttributes other) => Time.CompareTo(other.Time); + } +} diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 82e90399c9..b7529f39ca 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Edit } /// - /// Invokes , + /// Invokes , /// refreshing and parameters for the . /// protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty); diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs index 13cc41f8e0..dd2ad2cbfa 100644 --- a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs +++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs @@ -14,15 +14,15 @@ namespace osu.Game.Rulesets.Filter public interface IRulesetFilterCriteria { /// - /// Checks whether the supplied satisfies ruleset-specific custom criteria, + /// Checks whether the supplied satisfies ruleset-specific custom criteria, /// in addition to the ones mandated by song select. /// - /// The beatmap to test the criteria against. + /// The beatmap to test the criteria against. /// /// true if the beatmap matches the ruleset-specific custom filtering criteria, /// false otherwise. /// - bool Matches(BeatmapInfo beatmap); + bool Matches(BeatmapInfo beatmapInfo); /// /// Attempts to parse a single custom keyword criterion, given by the user via the song select search box. diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs new file mode 100644 index 0000000000..779433dc81 --- /dev/null +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Database; + +#nullable enable + +namespace osu.Game.Rulesets +{ + /// + /// A representation of a ruleset's metadata. + /// + public interface IRulesetInfo : IHasOnlineID + { + /// + /// The user-exposed name of this ruleset. + /// + string Name { get; } + + /// + /// An acronym defined by the ruleset that can be used as a permanent identifier. + /// + string ShortName { get; } + + /// + /// A string representation of this ruleset, to be used with reflection to instantiate the ruleset represented by this metadata. + /// + string InstantiationInfo { get; } + + Ruleset? CreateInstance() + { + var type = Type.GetType(InstantiationInfo); + + if (type == null) + return null; + + var ruleset = Activator.CreateInstance(type) as Ruleset; + + // overwrite the pre-populated RulesetInfo with a potentially database attached copy. + // TODO: figure if we still want/need this after switching to realm. + // ruleset.RulesetInfo = this; + + return ruleset; + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index b78c30e8a5..eefa1531c4 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Mods } } - public void ReadFromDifficulty(BeatmapDifficulty difficulty) + public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) { } diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 4edcb0b074..da838f9ea6 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Everything just got a bit harder..."; public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) }; - public void ReadFromDifficulty(BeatmapDifficulty difficulty) + public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) { } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index ae0cb895bc..0b159819d4 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Objects /// The control points. /// The difficulty settings to use. /// The cancellation token. - public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty, CancellationToken cancellationToken = default) + public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default) { ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Objects DefaultsApplied?.Invoke(this); } - protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { Kiai = controlPointInfo.EffectPointAt(StartTime + control_point_leniency).KiaiMode; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index df569b91c1..e1de82ade7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Objects.Legacy public double Velocity = 1; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 59ec9cdd7e..ca6a083a58 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -10,7 +10,7 @@ using osu.Framework.Testing; namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] - public class RulesetInfo : IEquatable + public class RulesetInfo : IEquatable, IRulesetInfo { public int? ID { get; set; } @@ -54,5 +54,11 @@ namespace osu.Game.Rulesets } public override string ToString() => Name ?? $"{Name} ({ShortName}) ID: {ID}"; + + #region Implementation of IHasOnlineID + + public int? OnlineID => ID; + + #endregion } } diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index cae41e22f4..85693abb93 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Scoring .First() ))); - targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); + targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.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/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 410614de07..3ffd1eb66b 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Scoring { foreach (var range in GetRanges()) { - var value = BeatmapDifficulty.DifficultyRange(difficulty, (range.Min, range.Average, range.Max)); + var value = IBeatmapDifficultyInfo.DifficultyRange(difficulty, (range.Min, range.Average, range.Max)); switch (range.Result) { diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 201a05e569..ed4a16f0e8 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -18,6 +18,11 @@ namespace osu.Game.Rulesets.Scoring /// public event Action NewJudgement; + /// + /// Invoked when a judgement is reverted, usually due to rewinding gameplay. + /// + public event Action JudgementReverted; + /// /// The maximum number of hits that can be judged. /// @@ -71,6 +76,8 @@ namespace osu.Game.Rulesets.Scoring JudgedHits--; RevertResultInternal(result); + + JudgementReverted?.Invoke(result); } /// diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index b57c224059..976f95cef8 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.UI private SpectatorClient spectatorClient { get; set; } [Resolved] - private GameplayBeatmap gameplayBeatmap { get; set; } + private GameplayState gameplayState { get; set; } protected ReplayRecorder(Score target) { @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.UI inputManager = GetContainingInputManager(); - spectatorClient?.BeginPlaying(gameplayBeatmap, target); + spectatorClient?.BeginPlaying(gameplayState, target); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 2e1a29372d..a1658b4cf3 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -70,7 +70,7 @@ namespace osu.Game.Scoring.Legacy scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.CreateMod()).ToArray(); currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); - scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; + scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo; /* score.HpGraphString = */ sr.ReadString(); @@ -119,7 +119,7 @@ namespace osu.Game.Scoring.Legacy // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. - score.ScoreInfo.Beatmap = workingBeatmap.BeatmapInfo; + score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo; return score; } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 288552879c..58e4192f77 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring.Legacy this.score = score; this.beatmap = beatmap; - if (score.ScoreInfo.Beatmap.RulesetID < 0 || score.ScoreInfo.Beatmap.RulesetID > 3) + if (score.ScoreInfo.BeatmapInfo.RulesetID < 0 || score.ScoreInfo.BeatmapInfo.RulesetID > 3) throw new ArgumentException("Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } @@ -44,7 +44,7 @@ namespace osu.Game.Scoring.Legacy { sw.Write((byte)(score.ScoreInfo.Ruleset.ID ?? 0)); sw.Write(LATEST_VERSION); - sw.Write(score.ScoreInfo.Beatmap.MD5Hash); + sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash); sw.Write(score.ScoreInfo.UserString); sw.Write($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}".ComputeMD5Hash()); sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0)); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 890ead40e3..5cf22f7945 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -150,7 +150,8 @@ namespace osu.Game.Scoring public int BeatmapInfoID { get; set; } [JsonIgnore] - public virtual BeatmapInfo Beatmap { get; set; } + [Column("Beatmap")] + public virtual BeatmapInfo BeatmapInfo { get; set; } [JsonIgnore] public long? OnlineScoreID { get; set; } @@ -252,7 +253,7 @@ namespace osu.Game.Scoring return clone; } - public override string ToString() => $"{User} playing {Beatmap}"; + public override string ToString() => $"{User} playing {BeatmapInfo}"; public bool Equals(ScoreInfo other) { diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 81e701f001..dde956233b 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -9,102 +9,48 @@ using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; using osu.Framework.Bindables; -using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring.Legacy; namespace osu.Game.Scoring { - public class ScoreManager : DownloadableArchiveModelManager + public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles, IPostImports { - public override IEnumerable HandledExtensions => new[] { ".osr" }; - - protected override string[] HashableFileTypes => new[] { ".osr" }; - - protected override string ImportFromStablePath => Path.Combine("Data", "r"); - - private readonly RulesetStore rulesets; - private readonly Func beatmaps; private readonly Scheduler scheduler; - - [CanBeNull] private readonly Func difficulties; - - [CanBeNull] private readonly OsuConfigManager configManager; + private readonly ScoreModelManager scoreModelManager; + private readonly ScoreModelDownloader scoreModelDownloader; public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, Scheduler scheduler, IIpcHost importHost = null, Func difficulties = null, OsuConfigManager configManager = null) - : base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost) { - this.rulesets = rulesets; - this.beatmaps = beatmaps; this.scheduler = scheduler; this.difficulties = difficulties; this.configManager = configManager; + + scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, contextFactory, importHost); + scoreModelDownloader = new ScoreModelDownloader(scoreModelManager, api, importHost); } - protected override ScoreInfo CreateModel(ArchiveReader archive) - { - if (archive == null) - return null; + public Score GetScore(ScoreInfo score) => scoreModelManager.GetScore(score); - using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) - { - try - { - return new DatabasedLegacyScoreDecoder(rulesets, beatmaps()).Parse(stream).ScoreInfo; - } - catch (LegacyScoreDecoder.BeatmapNotFoundException e) - { - Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error); - return null; - } - } - } + public List GetAllUsableScores() => scoreModelManager.GetAllUsableScores(); - protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) - => Task.CompletedTask; + public IEnumerable QueryScores(Expression> query) => scoreModelManager.QueryScores(query); - protected override void ExportModelTo(ScoreInfo model, Stream outputStream) - { - var file = model.Files.SingleOrDefault(); - if (file == null) - return; - - using (var inputStream = Files.Storage.GetStream(file.FileInfo.StoragePath)) - inputStream.CopyTo(outputStream); - } - - protected override IEnumerable GetStableImportPaths(Storage storage) - => storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) - .Select(path => storage.GetFullPath(path)); - - public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); - - public List GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); - - public IEnumerable QueryScores(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query); - - public ScoreInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); - - protected override ArchiveDownloadRequest CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score); - - protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) - => base.CheckLocalAvailability(model, items) - || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); + public ScoreInfo Query(Expression> query) => scoreModelManager.Query(query); /// /// Orders an array of s by total score. @@ -121,7 +67,7 @@ namespace osu.Game.Scoring // Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below. foreach (var s in scores) { - await difficultyCache.GetDifficultyAsync(s.Beatmap, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); + await difficultyCache.GetDifficultyAsync(s.BeatmapInfo, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } } @@ -180,7 +126,7 @@ namespace osu.Game.Scoring /// The total score. public async Task GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) { - if (score.Beatmap == null) + if (score.BeatmapInfo == null) return score.TotalScore; int beatmapMaxCombo; @@ -201,18 +147,18 @@ namespace osu.Game.Scoring // This score is guaranteed to be an osu!stable score. // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used. - if (score.Beatmap.MaxCombo != null) - beatmapMaxCombo = score.Beatmap.MaxCombo.Value; + if (score.BeatmapInfo.MaxCombo != null) + beatmapMaxCombo = score.BeatmapInfo.MaxCombo.Value; else { - if (score.Beatmap.ID == 0 || difficulties == null) + if (score.BeatmapInfo.ID == 0 || difficulties == null) { // We don't have enough information (max combo) to compute the score, so use the provided score. return score.TotalScore; } // We can compute the max combo locally after the async beatmap difficulty computation. - var difficulty = await difficulties().GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); + var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); beatmapMaxCombo = difficulty.MaxCombo; } } @@ -281,5 +227,149 @@ namespace osu.Game.Scoring this.totalScore.BindValueChanged(v => Value = v.NewValue.ToString("N0"), true); } } + + #region Implementation of IPostNotifications + + public Action PostNotification + { + set + { + scoreModelManager.PostNotification = value; + scoreModelDownloader.PostNotification = value; + } + } + + #endregion + + #region Implementation of IModelManager + + public IBindable> ItemUpdated => scoreModelManager.ItemUpdated; + + public IBindable> ItemRemoved => scoreModelManager.ItemRemoved; + + public Task ImportFromStableAsync(StableStorage stableStorage) + { + return scoreModelManager.ImportFromStableAsync(stableStorage); + } + + public void Export(ScoreInfo item) + { + scoreModelManager.Export(item); + } + + public void ExportModelTo(ScoreInfo model, Stream outputStream) + { + scoreModelManager.ExportModelTo(model, outputStream); + } + + public void Update(ScoreInfo item) + { + scoreModelManager.Update(item); + } + + public bool Delete(ScoreInfo item) + { + return scoreModelManager.Delete(item); + } + + public void Delete(List items, bool silent = false) + { + scoreModelManager.Delete(items, silent); + } + + public void Undelete(List items, bool silent = false) + { + scoreModelManager.Undelete(items, silent); + } + + public void Undelete(ScoreInfo item) + { + scoreModelManager.Undelete(item); + } + + public Task Import(params string[] paths) + { + return scoreModelManager.Import(paths); + } + + public Task Import(params ImportTask[] tasks) + { + return scoreModelManager.Import(tasks); + } + + public IEnumerable HandledExtensions => scoreModelManager.HandledExtensions; + + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) + { + return scoreModelManager.Import(notification, tasks); + } + + public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return scoreModelManager.Import(task, lowPriority, cancellationToken); + } + + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return scoreModelManager.Import(archive, lowPriority, cancellationToken); + } + + public Task> Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return scoreModelManager.Import(item, archive, lowPriority, cancellationToken); + } + + public bool IsAvailableLocally(ScoreInfo model) + { + return scoreModelManager.IsAvailableLocally(model); + } + + #endregion + + #region Implementation of IModelFileManager + + public void ReplaceFile(ScoreInfo model, ScoreFileInfo file, Stream contents, string filename = null) + { + scoreModelManager.ReplaceFile(model, file, contents, filename); + } + + public void DeleteFile(ScoreInfo model, ScoreFileInfo file) + { + scoreModelManager.DeleteFile(model, file); + } + + public void AddFile(ScoreInfo model, Stream contents, string filename) + { + scoreModelManager.AddFile(model, contents, filename); + } + + #endregion + + #region Implementation of IModelDownloader + + public IBindable>> DownloadBegan => scoreModelDownloader.DownloadBegan; + + public IBindable>> DownloadFailed => scoreModelDownloader.DownloadFailed; + + public bool Download(ScoreInfo model, bool minimiseDownloadSize) + { + return scoreModelDownloader.Download(model, minimiseDownloadSize); + } + + public ArchiveDownloadRequest GetExistingDownload(ScoreInfo model) + { + return scoreModelDownloader.GetExistingDownload(model); + } + + #endregion + + #region Implementation of IPresentImports + + public Action>> PostImport + { + set => scoreModelManager.PostImport = value; + } + + #endregion } } diff --git a/osu.Game/Scoring/ScoreModelDownloader.cs b/osu.Game/Scoring/ScoreModelDownloader.cs new file mode 100644 index 0000000000..b3c1e2928a --- /dev/null +++ b/osu.Game/Scoring/ScoreModelDownloader.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Scoring +{ + public class ScoreModelDownloader : ModelDownloader + { + public ScoreModelDownloader(ScoreModelManager scoreManager, IAPIProvider api, IIpcHost importHost = null) + : base(scoreManager, api, importHost) + { + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score); + } +} diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs new file mode 100644 index 0000000000..c65a6acdfb --- /dev/null +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -0,0 +1,88 @@ +// 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.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.IO.Archives; +using osu.Game.Rulesets; +using osu.Game.Scoring.Legacy; + +namespace osu.Game.Scoring +{ + public class ScoreModelManager : ArchiveModelManager + { + public override IEnumerable HandledExtensions => new[] { ".osr" }; + + protected override string[] HashableFileTypes => new[] { ".osr" }; + + protected override string ImportFromStablePath => Path.Combine("Data", "r"); + + private readonly RulesetStore rulesets; + private readonly Func beatmaps; + + public ScoreModelManager(RulesetStore rulesets, Func beatmaps, Storage storage, IDatabaseContextFactory contextFactory, IIpcHost importHost = null) + : base(storage, contextFactory, new ScoreStore(contextFactory, storage), importHost) + { + this.rulesets = rulesets; + this.beatmaps = beatmaps; + } + + protected override ScoreInfo CreateModel(ArchiveReader archive) + { + if (archive == null) + return null; + + using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) + { + try + { + return new DatabasedLegacyScoreDecoder(rulesets, beatmaps()).Parse(stream).ScoreInfo; + } + catch (LegacyScoreDecoder.BeatmapNotFoundException e) + { + Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error); + return null; + } + } + } + + public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); + + public List GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + + public IEnumerable QueryScores(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query); + + public ScoreInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); + + protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) + => Task.CompletedTask; + + protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) + => base.CheckLocalAvailability(model, items) + || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); + + public override void ExportModelTo(ScoreInfo model, Stream outputStream) + { + var file = model.Files.SingleOrDefault(); + if (file == null) + return; + + using (var inputStream = Files.Storage.GetStream(file.FileInfo.StoragePath)) + inputStream.CopyTo(outputStream); + } + + protected override IEnumerable GetStableImportPaths(Storage storage) + => storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) + .Select(path => storage.GetFullPath(path)); + } +} diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index bb15983de3..82685e9a04 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring { var score = lookup.ScoreInfo; - var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token).ConfigureAwait(false); + var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, token).ConfigureAwait(false); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. if (attributes.Attributes == null) diff --git a/osu.Game/Scoring/ScoreStore.cs b/osu.Game/Scoring/ScoreStore.cs index f5c5cd5dad..fd1f5ae3ec 100644 --- a/osu.Game/Scoring/ScoreStore.cs +++ b/osu.Game/Scoring/ScoreStore.cs @@ -17,9 +17,9 @@ namespace osu.Game.Scoring protected override IQueryable AddIncludesForConsumption(IQueryable query) => base.AddIncludesForConsumption(query) - .Include(s => s.Beatmap) - .Include(s => s.Beatmap).ThenInclude(b => b.Metadata) - .Include(s => s.Beatmap).ThenInclude(b => b.BeatmapSet).ThenInclude(s => s.Metadata) + .Include(s => s.BeatmapInfo) + .Include(s => s.BeatmapInfo).ThenInclude(b => b.Metadata) + .Include(s => s.BeatmapInfo).ThenInclude(b => b.BeatmapSet).ThenInclude(s => s.Metadata) .Include(s => s.Ruleset); } } diff --git a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs index 5f9b72447b..c458b65607 100644 --- a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs +++ b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs @@ -10,12 +10,12 @@ namespace osu.Game.Screens.Edit.Components.Menus { public class DifficultyMenuItem : StatefulMenuItem { - public BeatmapInfo Beatmap { get; } + public BeatmapInfo BeatmapInfo { get; } public DifficultyMenuItem(BeatmapInfo beatmapInfo, bool selected, Action difficultyChangeFunc) : base(beatmapInfo.Version ?? "(unnamed)", null) { - Beatmap = beatmapInfo; + BeatmapInfo = beatmapInfo; State.Value = selected; if (!selected) diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index fd43349793..f833bc49f7 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -89,6 +89,13 @@ namespace osu.Game.Screens.Edit.Setup { public Action OnFocused; + protected override bool OnDragStart(DragStartEvent e) + { + // This text box is intended to be "read only" without actually specifying that. + // As such we don't want to allow the user to select its content with a drag. + return false; + } + protected override void OnFocus(FocusEvent e) { OnFocused?.Invoke(); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index cfe14eab92..fbd33cad67 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -101,8 +101,12 @@ namespace osu.Game.Screens.Menu // if we detect that the theme track or beatmap is unavailable this is either first startup or things are in a bad state. // this could happen if a user has nuked their files store. for now, reimport to repair this. var import = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).Result; - import.Protected = true; - beatmaps.Update(import); + + import.PerformWrite(b => + { + b.Protected = true; + beatmaps.Update(b); + }); loadThemedIntro(); } diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index a64d89b699..381849189d 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Components req.Failure += exception => { - onError?.Invoke(req.Result?.Error ?? exception.Message); + onError?.Invoke(req.Response?.Error ?? exception.Message); }; api.Queue(req); diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 69eb857661..585b024623 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) }; beatmapText.Clear(); - beatmapText.AddLink(Item.Beatmap.Value.ToRomanisableString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text => + beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text => { text.Truncate = true; text.RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Screens/OnlinePlay/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs index b0db9256f5..2d4b5cc527 100644 --- a/osu.Game/Screens/OnlinePlay/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Header.cs @@ -72,21 +72,21 @@ namespace osu.Game.Screens.OnlinePlay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 24), + Font = OsuFont.TorusAlternate.With(size: 24), Text = mainTitle }, dot = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 24), + Font = OsuFont.TorusAlternate.With(size: 24), Text = "·" }, pageTitle = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 24), + Font = OsuFont.TorusAlternate.With(size: 24), Text = "Lounge" } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 80070aa6ba..acd87ed864 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -14,6 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; @@ -38,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private readonly Bindable roomCategory = new Bindable(); private readonly Bindable hasPassword = new Bindable(); - private RecentParticipantsList recentParticipantsList; + private DrawableRoomParticipantsList drawableRoomParticipantsList; private RoomSpecialCategoryPill specialCategoryPill; private PasswordProtectedIcon passwordIcon; private EndDateInfo endDateInfo; @@ -136,7 +137,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { new FillFlowContainer { - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Children = new Drawable[] { @@ -166,13 +168,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }, new FillFlowContainer { - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Top = 3 }, Direction = FillDirection.Vertical, Children = new Drawable[] { new RoomNameText(), - new RoomHostText(), + new RoomStatusText() } } }, @@ -217,7 +220,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Children = new Drawable[] { ButtonsContainer, - recentParticipantsList = new RecentParticipantsList + drawableRoomParticipantsList = new DrawableRoomParticipantsList { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, @@ -280,8 +283,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { numberOfAvatars = value; - if (recentParticipantsList != null) - recentParticipantsList.NumberOfCircles = value; + if (drawableRoomParticipantsList != null) + drawableRoomParticipantsList.NumberOfCircles = value; } } @@ -304,38 +307,87 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - private class RoomHostText : OnlinePlayComposite + private class RoomStatusText : OnlinePlayComposite { - private LinkFlowContainer hostText; + [Resolved] + private OsuColour colours { get; set; } - public RoomHostText() + private SpriteText statusText; + private LinkFlowContainer beatmapText; + + public RoomStatusText() { - AutoSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Width = 0.5f; } [BackgroundDependencyLoader] private void load() { - InternalChild = hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 16)) + InternalChild = new GridContainer { - AutoSizeAxes = Axes.Both + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + statusText = new OsuSpriteText + { + Font = OsuFont.Default.With(size: 16), + Colour = colours.Lime1 + }, + beatmapText = new LinkFlowContainer(s => + { + s.Font = OsuFont.Default.With(size: 16); + s.Colour = colours.Lime1; + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + } }; } protected override void LoadComplete() { base.LoadComplete(); + SelectedItem.BindValueChanged(onSelectedItemChanged, true); + } - Host.BindValueChanged(host => + private void onSelectedItemChanged(ValueChangedEvent item) + { + beatmapText.Clear(); + + if (Type.Value == MatchType.Playlists) { - hostText.Clear(); + statusText.Text = "Ready to play"; + return; + } - if (host.NewValue != null) - { - hostText.AddText("hosted by "); - hostText.AddUserLink(host.NewValue); - } - }, true); + if (item.NewValue?.Beatmap.Value != null) + { + statusText.Text = "Currently playing "; + beatmapText.AddLink(item.NewValue.Beatmap.Value.GetDisplayTitleRomanisable(), + LinkAction.OpenBeatmap, + item.NewValue.Beatmap.Value.OnlineBeatmapID.ToString(), + creationParameters: s => + { + s.Truncate = true; + s.RelativeSizeAxes = Axes.X; + }); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs similarity index 58% rename from osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index bc658f45e4..31eb5db9bc 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -4,11 +4,13 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Users; @@ -17,16 +19,18 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class RecentParticipantsList : OnlinePlayComposite + public class DrawableRoomParticipantsList : OnlinePlayComposite { private const float avatar_size = 36; private FillFlowContainer avatarFlow; + private CircularAvatar hostAvatar; + private LinkFlowContainer hostText; private HiddenUserCount hiddenUsers; private OsuSpriteText totalCount; - public RecentParticipantsList() + public DrawableRoomParticipantsList() { AutoSizeAxes = Axes.X; Height = 60; @@ -51,42 +55,98 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }, new FillFlowContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4), - Padding = new MarginPadding { Right = 16 }, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, Children = new Drawable[] { - new SpriteIcon + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(16), - Margin = new MarginPadding { Left = 8 }, - Icon = FontAwesome.Solid.User, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Spacing = new Vector2(8), + Padding = new MarginPadding + { + Left = 8, + Right = 16 + }, + Children = new Drawable[] + { + hostAvatar = new CircularAvatar + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + hostText = new LinkFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both + } + } }, - totalCount = new OsuSpriteText + new Container { - Font = OsuFont.Default.With(weight: FontWeight.Bold), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Shear = new Vector2(0.2f, 0), + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Background3, + } + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Padding = new MarginPadding + { + Left = 8, + Right = 16 + }, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(16), + Icon = FontAwesome.Solid.User, + }, + totalCount = new OsuSpriteText + { + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + avatarFlow = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Margin = new MarginPadding { Left = 4 }, + }, + hiddenUsers = new HiddenUserCount + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + } + } }, - avatarFlow = new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4), - Margin = new MarginPadding { Left = 4 }, - }, - hiddenUsers = new HiddenUserCount - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } } } }; @@ -102,6 +162,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components updateHiddenUsers(); totalCount.Text = ParticipantCount.Value.ToString(); }, true); + + Host.BindValueChanged(onHostChanged, true); } private int numberOfCircles = 4; @@ -194,6 +256,18 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + private void onHostChanged(ValueChangedEvent host) + { + hostAvatar.User = host.NewValue; + hostText.Clear(); + + if (host.NewValue != null) + { + hostText.AddText("hosted by "); + hostText.AddUserLink(host.NewValue); + } + } + private class CircularAvatar : CompositeDrawable { public User User diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index 6f1817a77c..35f30edf65 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -12,7 +12,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class BeatmapSelectionControl : RoomSubScreenComposite + public class BeatmapSelectionControl : OnlinePlayComposite { [Resolved] private MultiplayerMatchSubScreen matchSubScreen { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index c45e3a79da..7bf8ce0e1a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -213,8 +213,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } - protected override void StartGameplay(int userId, GameplayState gameplayState) - => instances.Single(i => i.UserId == userId).LoadScore(gameplayState.Score); + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) + => instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score); protected override void EndGameplay(int userId) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 95ccc08608..c3190cd845 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Score = score; - gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) + gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, Child = stack = new OsuScreenStack() diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 24b3b4ec94..aa971864ef 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -65,8 +65,11 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable Duration { get; private set; } + [Resolved(CanBeNull = true)] + private IBindable subScreenSelectedItem { get; set; } + /// - /// The currently selected item in the , or the first item from + /// The currently selected item in the , or the last item from /// if this is not within a . /// protected readonly Bindable SelectedItem = new Bindable(); @@ -75,12 +78,13 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); + subScreenSelectedItem?.BindValueChanged(_ => UpdateSelectedItem()); Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true); } protected virtual void UpdateSelectedItem() - { - SelectedItem.Value = Playlist.FirstOrDefault(); - } + => SelectedItem.Value = RoomID.Value == null || subScreenSelectedItem == null + ? Playlist.LastOrDefault() + : subScreenSelectedItem.Value; } } diff --git a/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs b/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs deleted file mode 100644 index 4cfd881aa3..0000000000 --- a/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Match; - -namespace osu.Game.Screens.OnlinePlay -{ - /// - /// An with additional logic tracking the currently-selected inside a . - /// - public class RoomSubScreenComposite : OnlinePlayComposite - { - [Resolved] - private IBindable subScreenSelectedItem { get; set; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - subScreenSelectedItem.BindValueChanged(_ => UpdateSelectedItem(), true); - } - - protected override void UpdateSelectedItem() - { - if (RoomID.Value == null) - { - // If the room hasn't been created yet, fall-back to the base logic. - base.UpdateSelectedItem(); - return; - } - - SelectedItem.Value = subScreenSelectedItem.Value; - } - } -} diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs deleted file mode 100644 index 74fbe540fa..0000000000 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Timing; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; - -namespace osu.Game.Screens.Play -{ - public class GameplayBeatmap : Component, IBeatmap - { - public readonly IBeatmap PlayableBeatmap; - - public GameplayBeatmap(IBeatmap playableBeatmap) - { - PlayableBeatmap = playableBeatmap; - } - - public BeatmapInfo BeatmapInfo - { - get => PlayableBeatmap.BeatmapInfo; - set => PlayableBeatmap.BeatmapInfo = value; - } - - public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; - - public ControlPointInfo ControlPointInfo - { - get => PlayableBeatmap.ControlPointInfo; - set => PlayableBeatmap.ControlPointInfo = value; - } - - public List Breaks => PlayableBeatmap.Breaks; - - public double TotalBreakTime => PlayableBeatmap.TotalBreakTime; - - public IReadOnlyList HitObjects => PlayableBeatmap.HitObjects; - - public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics(); - - public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength(); - - public IBeatmap Clone() => PlayableBeatmap.Clone(); - - private readonly Bindable lastJudgementResult = new Bindable(); - - public IBindable LastJudgementResult => lastJudgementResult; - - public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result; - } -} diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs new file mode 100644 index 0000000000..44f72022f7 --- /dev/null +++ b/osu.Game/Screens/Play/GameplayState.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 System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; + +#nullable enable + +namespace osu.Game.Screens.Play +{ + /// + /// The state of an active gameplay session, generally constructed and exposed by . + /// + public class GameplayState + { + /// + /// The final post-convert post-mod-application beatmap. + /// + public readonly IBeatmap Beatmap; + + /// + /// The ruleset used in gameplay. + /// + public readonly Ruleset Ruleset; + + /// + /// The mods applied to the gameplay. + /// + public readonly IReadOnlyList Mods; + + /// + /// The gameplay score. + /// + public readonly Score Score; + + /// + /// A bindable tracking the last judgement result applied to any hit object. + /// + public IBindable LastJudgementResult => lastJudgementResult; + + private readonly Bindable lastJudgementResult = new Bindable(); + + public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList? mods = null, Score? score = null) + { + Beatmap = beatmap; + Ruleset = ruleset; + Score = score ?? new Score(); + Mods = mods ?? ArraySegment.Empty; + } + + /// + /// Applies the score change of a to this . + /// + /// The to apply. + public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result; + } +} diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs new file mode 100644 index 0000000000..ef289c2a20 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -0,0 +1,227 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public class PerformancePointsCounter : RollingCounter, ISkinnableDrawable + { + public bool UsesFixedAnchor { get; set; } + + protected override bool IsRollingProportional => true; + + protected override double RollingDuration => 1000; + + private const float alpha_when_invalid = 0.3f; + + [CanBeNull] + [Resolved(CanBeNull = true)] + private ScoreProcessor scoreProcessor { get; set; } + + [Resolved(CanBeNull = true)] + [CanBeNull] + private GameplayState gameplayState { get; set; } + + [CanBeNull] + private List timedAttributes; + + private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); + + private JudgementResult lastJudgement; + + public PerformancePointsCounter() + { + Current.Value = DisplayedCount = 0; + } + + private Mod[] clonedMods; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache) + { + Colour = colours.BlueLighter; + + if (gameplayState != null) + { + clonedMods = gameplayState.Mods.Select(m => m.DeepClone()).ToArray(); + + var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); + difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, clonedMods, loadCancellationSource.Token) + .ContinueWith(r => Schedule(() => + { + timedAttributes = r.Result; + IsValid = true; + if (lastJudgement != null) + onJudgementChanged(lastJudgement); + }), TaskContinuationOptions.OnlyOnRanToCompletion); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (scoreProcessor != null) + { + scoreProcessor.NewJudgement += onJudgementChanged; + scoreProcessor.JudgementReverted += onJudgementChanged; + } + } + + private bool isValid; + + protected bool IsValid + { + set + { + if (value == isValid) + return; + + isValid = value; + DrawableCount.FadeTo(isValid ? 1 : alpha_when_invalid, 1000, Easing.OutQuint); + } + } + + private void onJudgementChanged(JudgementResult judgement) + { + lastJudgement = judgement; + + var attrib = getAttributeAtTime(judgement); + + if (gameplayState == null || attrib == null) + { + IsValid = false; + return; + } + + // awkward but we need to make sure the true mods are not passed to PerformanceCalculator as it makes a mess of track applications. + var scoreInfo = gameplayState.Score.ScoreInfo.DeepClone(); + scoreInfo.Mods = clonedMods; + + var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, scoreInfo); + + Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero); + IsValid = true; + } + + [CanBeNull] + private DifficultyAttributes getAttributeAtTime(JudgementResult judgement) + { + if (timedAttributes == null || timedAttributes.Count == 0) + return null; + + int attribIndex = timedAttributes.BinarySearch(new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + if (attribIndex < 0) + attribIndex = ~attribIndex - 1; + + return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Count - 1)].Attributes; + } + + protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); + + protected override IHasText CreateText() => new TextComponent + { + Alpha = alpha_when_invalid + }; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (scoreProcessor != null) + scoreProcessor.NewJudgement -= onJudgementChanged; + + loadCancellationSource?.Cancel(); + } + + private class TextComponent : CompositeDrawable, IHasText + { + public LocalisableString Text + { + get => text.Text; + set => text.Text = value; + } + + private readonly OsuSpriteText text; + + public TextComponent() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(2), + Children = new Drawable[] + { + text = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Numeric.With(size: 16) + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = @"pp", + Font = OsuFont.Numeric.With(size: 8), + Padding = new MarginPadding { Bottom = 1.5f }, // align baseline better + } + } + }; + } + } + + // TODO: This class shouldn't exist, but requires breaking changes to allow DifficultyCalculator to receive an IBeatmap. + private class GameplayWorkingBeatmap : WorkingBeatmap + { + private readonly IBeatmap gameplayBeatmap; + + public GameplayWorkingBeatmap(IBeatmap gameplayBeatmap) + : base(gameplayBeatmap.BeatmapInfo, null) + { + this.gameplayBeatmap = gameplayBeatmap; + } + + public override IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) + => gameplayBeatmap; + + protected override IBeatmap GetBeatmap() => gameplayBeatmap; + + protected override Texture GetBackground() => throw new NotImplementedException(); + + protected override Track GetBeatmapTrack() => throw new NotImplementedException(); + + protected internal override ISkin GetSkin() => throw new NotImplementedException(); + + public override Stream GetStream(string storagePath) => throw new NotImplementedException(); + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9927467bd6..e2ed71e28a 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; @@ -93,9 +92,9 @@ namespace osu.Game.Screens.Play [Resolved] private SpectatorClient spectatorClient { get; set; } - protected Ruleset GameplayRuleset { get; private set; } + public GameplayState GameplayState { get; private set; } - protected GameplayBeatmap GameplayBeatmap { get; private set; } + private Ruleset ruleset; private Sample sampleRestart; @@ -125,15 +124,11 @@ namespace osu.Game.Screens.Play 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; @@ -161,13 +156,6 @@ namespace osu.Game.Screens.Play if (!LoadedBeatmapSuccessfully) return; - Score = CreateScore(); - - // ensure the score is in a consistent state with the current player. - Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo; - Score.ScoreInfo.Ruleset = GameplayRuleset.RulesetInfo; - Score.ScoreInfo.Mods = Mods.Value.ToArray(); - PrepareReplay(); ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo); @@ -186,12 +174,12 @@ namespace osu.Game.Screens.Play [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; @@ -206,16 +194,16 @@ namespace osu.Game.Screens.Play if (game is OsuGame osuGame) LocalUserPlaying.BindTo(osuGame.LocalUserPlaying); - DrawableRuleset = GameplayRuleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); + DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, gameplayMods); dependencies.CacheAs(DrawableRuleset); - ScoreProcessor = GameplayRuleset.CreateScoreProcessor(); + ScoreProcessor = ruleset.CreateScoreProcessor(); ScoreProcessor.ApplyBeatmap(playableBeatmap); - ScoreProcessor.Mods.BindTo(Mods); + ScoreProcessor.Mods.Value = gameplayMods; dependencies.CacheAs(ScoreProcessor); - HealthProcessor = GameplayRuleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); + HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor.ApplyBeatmap(playableBeatmap); dependencies.CacheAs(HealthProcessor); @@ -225,12 +213,18 @@ namespace osu.Game.Screens.Play InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); - AddInternal(GameplayBeatmap = new GameplayBeatmap(playableBeatmap)); + 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 = gameplayMods; + + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score)); + AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); - dependencies.CacheAs(GameplayBeatmap); - - var rulesetSkinProvider = new RulesetSkinProvidingContainer(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin); + var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); // load the skinning hierarchy first. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. @@ -280,7 +274,7 @@ namespace osu.Game.Screens.Play { HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); - GameplayBeatmap.ApplyResult(r); + GameplayState.ApplyResult(r); }; DrawableRuleset.RevertResult += r => @@ -303,13 +297,13 @@ namespace osu.Game.Screens.Play // 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 +351,7 @@ namespace osu.Game.Screens.Play // 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 = { @@ -468,7 +462,7 @@ namespace osu.Game.Screens.Play } } - private IBeatmap loadPlayableBeatmap() + private IBeatmap loadPlayableBeatmap(Mod[] gameplayMods) { IBeatmap playable; @@ -478,19 +472,19 @@ namespace osu.Game.Screens.Play throw new InvalidOperationException("Beatmap was not loaded"); var rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset; - GameplayRuleset = rulesetInfo.CreateInstance(); + ruleset = rulesetInfo.CreateInstance(); try { - playable = Beatmap.Value.GetPlayableBeatmap(GameplayRuleset.RulesetInfo, Mods.Value); + playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, gameplayMods); } catch (BeatmapInvalidForRulesetException) { // A playable beatmap may not be creatable with the user's preferred ruleset, so try using the beatmap's default ruleset rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset; - GameplayRuleset = rulesetInfo.CreateInstance(); + ruleset = rulesetInfo.CreateInstance(); - playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value); + playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, gameplayMods); } if (playable.HitObjects.Count == 0) @@ -790,7 +784,7 @@ namespace osu.Game.Screens.Play failAnimation.Start(); - if (Mods.Value.OfType().Any(m => m.RestartOnFail)) + if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) Restart(); return true; @@ -920,17 +914,17 @@ namespace osu.Game.Screens.Play 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(); @@ -989,8 +983,9 @@ namespace osu.Game.Screens.Play /// /// Creates the player's . /// + /// /// The . - protected virtual Score CreateScore() => new Score + protected virtual Score CreateScore(IBeatmap beatmap) => new Score { ScoreInfo = new ScoreInfo { User = api.LocalUser.Value }, }; @@ -1010,7 +1005,7 @@ namespace osu.Game.Screens.Play using (var stream = new MemoryStream()) { - new LegacyScoreEncoder(score, GameplayBeatmap.PlayableBeatmap).Encode(stream); + new LegacyScoreEncoder(score, GameplayState.Beatmap).Encode(stream); replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 0c6f1ed911..93054b7bb5 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(Score); } - protected override Score CreateScore() => createScore(GameplayBeatmap.PlayableBeatmap, Mods.Value); + protected override Score CreateScore(IBeatmap beatmap) => createScore(beatmap, Mods.Value); // Don't re-import replay scores as they're already present in the database. protected override Task ImportScore(Score score) => Task.CompletedTask; @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play void keyboardSeek(int direction) { - double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayBeatmap.HitObjects.Last().GetEndTime()); + double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.HitObjects.Last().GetEndTime()); Seek(target); } diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index d90e8e0168..675cb71311 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play protected override APIRequest CreateSubmissionRequest(Score score, long token) { - var beatmap = score.ScoreInfo.Beatmap; + var beatmap = score.ScoreInfo.BeatmapInfo; Debug.Assert(beatmap.OnlineBeatmapID != null); diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 4520e2e825..9d4dad8bdc 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Play /// The player's immediate online gameplay state. /// This doesn't always reflect the gameplay state being watched. /// - private GameplayState immediateGameplayState; + private SpectatorGameplayState immediateSpectatorGameplayState; private GetBeatmapSetRequest onlineBeatmapRequest; @@ -146,7 +146,7 @@ namespace osu.Game.Screens.Play Width = 250, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Action = () => scheduleStart(immediateGameplayState), + Action = () => scheduleStart(immediateSpectatorGameplayState), Enabled = { Value = false } } } @@ -167,18 +167,18 @@ namespace osu.Game.Screens.Play showBeatmapPanel(spectatorState); } - protected override void StartGameplay(int userId, GameplayState gameplayState) + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) { - immediateGameplayState = gameplayState; + immediateSpectatorGameplayState = spectatorGameplayState; watchButton.Enabled.Value = true; - scheduleStart(gameplayState); + scheduleStart(spectatorGameplayState); } protected override void EndGameplay(int userId) { scheduledStart?.Cancel(); - immediateGameplayState = null; + immediateSpectatorGameplayState = null; watchButton.Enabled.Value = false; clearDisplay(); @@ -194,7 +194,7 @@ namespace osu.Game.Screens.Play private ScheduledDelegate scheduledStart; - private void scheduleStart(GameplayState gameplayState) + private void scheduleStart(SpectatorGameplayState spectatorGameplayState) { // This function may be called multiple times in quick succession once the screen becomes current again. scheduledStart?.Cancel(); @@ -203,15 +203,15 @@ namespace osu.Game.Screens.Play if (this.IsCurrentScreen()) start(); else - scheduleStart(gameplayState); + scheduleStart(spectatorGameplayState); }); void start() { - Beatmap.Value = gameplayState.Beatmap; - Ruleset.Value = gameplayState.Ruleset.RulesetInfo; + Beatmap.Value = spectatorGameplayState.Beatmap; + Ruleset.Value = spectatorGameplayState.Ruleset.RulesetInfo; - this.Push(new SpectatorPlayerLoader(gameplayState.Score, () => new SoloSpectatorPlayer(gameplayState.Score))); + this.Push(new SpectatorPlayerLoader(spectatorGameplayState.Score, () => new SoloSpectatorPlayer(spectatorGameplayState.Score))); } } diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index d7e42a9cd1..f6a89e7fa9 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Spectator; @@ -66,8 +67,8 @@ namespace osu.Game.Screens.Play foreach (var frame in bundle.Frames) { - IConvertibleReplayFrame convertibleFrame = GameplayRuleset.CreateConvertibleReplayFrame(); - convertibleFrame.FromLegacy(frame, GameplayBeatmap.PlayableBeatmap); + IConvertibleReplayFrame convertibleFrame = GameplayState.Ruleset.CreateConvertibleReplayFrame(); + convertibleFrame.FromLegacy(frame, GameplayState.Beatmap); var convertedFrame = (ReplayFrame)convertibleFrame; convertedFrame.Time = frame.Time; @@ -79,7 +80,7 @@ namespace osu.Game.Screens.Play NonFrameStableSeek(score.Replay.Frames[0].Time); } - protected override Score CreateScore() => score; + protected override Score CreateScore(IBeatmap beatmap) => score; protected override ResultsScreen CreateResults(ScoreInfo score) => new SpectatorResultsScreen(score); diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index bcb5e7999f..262d1e8293 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Ranking.Expanded [BackgroundDependencyLoader] private void load(BeatmapDifficultyCache beatmapDifficultyCache) { - var beatmap = score.Beatmap; + var beatmap = score.BeatmapInfo; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; var creator = metadata.Author?.Username; diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 18b8649a59..e644eb671a 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Ranking break; case DownloadState.NotDownloaded: - scores.Download(Model.Value); + scores.Download(Model.Value, false); break; case DownloadState.Importing: diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 9bc696948f..5e582a8dcb 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -27,10 +27,10 @@ namespace osu.Game.Screens.Ranking protected override APIRequest FetchScores(Action> scoresCallback) { - if (Score.Beatmap.OnlineBeatmapID == null || Score.Beatmap.Status <= BeatmapSetOnlineStatus.Pending) + if (Score.BeatmapInfo.OnlineBeatmapID == null || Score.BeatmapInfo.Status <= BeatmapSetOnlineStatus.Pending) return null; - getScoreRequest = new GetScoresRequest(Score.Beatmap, Score.Ruleset); + getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets))); return getScoreRequest; } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index f1ae1f9d73..bc62bcf2b2 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -102,7 +102,7 @@ namespace osu.Game.Screens.Ranking.Statistics // Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events. Task.Run(() => { - playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.Beatmap).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty()); + playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty()); }, loadCancellation.Token).ContinueWith(t => Schedule(() => { var rows = new FillFlowContainer diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5eceae3c6e..e5e28d2fde 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Select /// /// The currently selected beatmap. /// - public BeatmapInfo SelectedBeatmap => selectedBeatmap?.Beatmap; + public BeatmapInfo SelectedBeatmapInfo => selectedBeatmap?.BeatmapInfo; private CarouselBeatmap selectedBeatmap => selectedBeatmapSet?.Beatmaps.FirstOrDefault(s => s.State.Value == CarouselItemState.Selected); @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Select private CarouselBeatmapSet selectedBeatmapSet; /// - /// Raised when the is changed. + /// Raised when the is changed. /// public Action SelectionChanged; @@ -212,7 +212,7 @@ namespace osu.Game.Screens.Select // If the selected beatmap is about to be removed, store its ID so it can be re-selected if required if (existingSet?.State?.Value == CarouselItemState.Selected) - previouslySelectedID = selectedBeatmap?.Beatmap.ID; + previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID; var newSet = createCarouselSet(beatmapSet); @@ -233,7 +233,7 @@ namespace osu.Game.Screens.Select // check if we can/need to maintain our current selection. if (previouslySelectedID != null) - select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet); + select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); itemsCache.Invalidate(); Schedule(() => BeatmapSetsChanged?.Invoke()); @@ -242,15 +242,15 @@ namespace osu.Game.Screens.Select /// /// Selects a given beatmap on the carousel. /// - /// The beatmap to select. + /// The beatmap to select. /// Whether to select the beatmap even if it is filtered (i.e., not visible on carousel). /// True if a selection was made, False if it wasn't. - public bool SelectBeatmap(BeatmapInfo beatmap, bool bypassFilters = true) + public bool SelectBeatmap(BeatmapInfo beatmapInfo, bool bypassFilters = true) { // ensure that any pending events from BeatmapManager have been run before attempting a selection. Scheduler.Update(); - if (beatmap?.Hidden != false) + if (beatmapInfo?.Hidden != false) return false; foreach (CarouselBeatmapSet set in beatmapSets) @@ -258,7 +258,7 @@ namespace osu.Game.Screens.Select if (!bypassFilters && set.Filtered.Value) continue; - var item = set.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); + var item = set.Beatmaps.FirstOrDefault(p => p.BeatmapInfo.Equals(beatmapInfo)); if (item == null) // The beatmap that needs to be selected doesn't exist in this set @@ -472,7 +472,7 @@ namespace osu.Game.Screens.Select private float? scrollTarget; /// - /// Scroll to the current . + /// Scroll to the current . /// /// /// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels. @@ -720,7 +720,7 @@ namespace osu.Game.Screens.Select if (state.NewValue == CarouselItemState.Selected) { selectedBeatmapSet = set; - SelectionChanged?.Invoke(c.Beatmap); + SelectionChanged?.Invoke(c.BeatmapInfo); itemsCache.Invalidate(); ScrollToSelected(); diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index b32416b361..4970db8955 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -17,9 +17,9 @@ namespace osu.Game.Screens.Select [Resolved] private ScoreManager scoreManager { get; set; } - public BeatmapClearScoresDialog(BeatmapInfo beatmap, Action onCompletion) + public BeatmapClearScoresDialog(BeatmapInfo beatmapInfo, Action onCompletion) { - BodyText = $@"{beatmap.Metadata?.Artist} - {beatmap.Metadata?.Title}"; + BodyText = $@"{beatmapInfo.Metadata?.Artist} - {beatmapInfo.Metadata?.Title}"; Icon = FontAwesome.Solid.Eraser; HeaderText = @"Clearing all local scores. Are you sure?"; Buttons = new PopupDialogButton[] @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select Text = @"Yes. Please.", Action = () => { - Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.Beatmap.ID == beatmap.ID).ToList())) + Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == beatmapInfo.ID).ToList())) .ContinueWith(_ => onCompletion); } }, diff --git a/osu.Game/Screens/Select/BeatmapDetailArea.cs b/osu.Game/Screens/Select/BeatmapDetailArea.cs index 89ae92ec91..72c2ba708b 100644 --- a/osu.Game/Screens/Select/BeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/BeatmapDetailArea.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Select { beatmap = value; - Details.Beatmap = value?.BeatmapInfo; + Details.BeatmapInfo = value?.BeatmapInfo; } } diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 973f54c038..6ace92370c 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -41,16 +41,16 @@ namespace osu.Game.Screens.Select [Resolved] private RulesetStore rulesets { get; set; } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; Scheduler.AddOnce(updateStatistics); } @@ -170,26 +170,26 @@ namespace osu.Game.Screens.Select private void updateStatistics() { - advanced.Beatmap = Beatmap; - description.Text = Beatmap?.Version; - source.Text = Beatmap?.Metadata?.Source; - tags.Text = Beatmap?.Metadata?.Tags; + advanced.BeatmapInfo = BeatmapInfo; + description.Text = BeatmapInfo?.Version; + source.Text = BeatmapInfo?.Metadata?.Source; + tags.Text = BeatmapInfo?.Metadata?.Tags; // metrics may have been previously fetched - if (Beatmap?.BeatmapSet?.Metrics != null && Beatmap?.Metrics != null) + if (BeatmapInfo?.BeatmapSet?.Metrics != null && BeatmapInfo?.Metrics != null) { updateMetrics(); return; } // for now, let's early abort if an OnlineBeatmapID is not present (should have been populated at import time). - if (Beatmap?.OnlineBeatmapID == null || api.State.Value == APIState.Offline) + if (BeatmapInfo?.OnlineBeatmapID == null || api.State.Value == APIState.Offline) { updateMetrics(); return; } - var requestedBeatmap = Beatmap; + var requestedBeatmap = BeatmapInfo; var lookup = new GetBeatmapRequest(requestedBeatmap); @@ -197,11 +197,11 @@ namespace osu.Game.Screens.Select { Schedule(() => { - if (beatmap != requestedBeatmap) + if (beatmapInfo != requestedBeatmap) // the beatmap has been changed since we started the lookup. return; - var b = res.ToBeatmap(rulesets); + var b = res.ToBeatmapInfo(rulesets); if (requestedBeatmap.BeatmapSet == null) requestedBeatmap.BeatmapSet = b.BeatmapSet; @@ -218,7 +218,7 @@ namespace osu.Game.Screens.Select { Schedule(() => { - if (beatmap != requestedBeatmap) + if (beatmapInfo != requestedBeatmap) // the beatmap has been changed since we started the lookup. return; @@ -232,12 +232,12 @@ namespace osu.Game.Screens.Select private void updateMetrics() { - var hasRatings = beatmap?.BeatmapSet?.Metrics?.Ratings?.Any() ?? false; - var hasRetriesFails = (beatmap?.Metrics?.Retries?.Any() ?? false) || (beatmap?.Metrics?.Fails?.Any() ?? false); + var hasRatings = beatmapInfo?.BeatmapSet?.Metrics?.Ratings?.Any() ?? false; + var hasRetriesFails = (beatmapInfo?.Metrics?.Retries?.Any() ?? false) || (beatmapInfo?.Metrics?.Fails?.Any() ?? false); if (hasRatings) { - ratings.Metrics = beatmap.BeatmapSet.Metrics; + ratings.Metrics = beatmapInfo.BeatmapSet.Metrics; ratings.FadeIn(transition_duration); } else @@ -249,7 +249,7 @@ namespace osu.Game.Screens.Select if (hasRetriesFails) { - failRetryGraph.Metrics = beatmap.Metrics; + failRetryGraph.Metrics = beatmapInfo.Metrics; failRetryContainer.FadeIn(transition_duration); } else diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index f95ddfee41..d8c5aa760e 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -12,11 +12,11 @@ namespace osu.Game.Screens.Select.Carousel { public override float TotalHeight => DrawableCarouselBeatmap.HEIGHT; - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; - public CarouselBeatmap(BeatmapInfo beatmap) + public CarouselBeatmap(BeatmapInfo beatmapInfo) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; State.Value = CarouselItemState.Collapsed; } @@ -28,36 +28,36 @@ namespace osu.Game.Screens.Select.Carousel bool match = criteria.Ruleset == null || - Beatmap.RulesetID == criteria.Ruleset.ID || - (Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); + BeatmapInfo.RulesetID == criteria.Ruleset.ID || + (BeatmapInfo.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); - if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) + if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { // only check ruleset equality or convertability for selected beatmap Filtered.Value = !match; return; } - match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(Beatmap.StarDifficulty); - match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate); - match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate); - match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(Beatmap.BaseDifficulty.CircleSize); - match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(Beatmap.BaseDifficulty.OverallDifficulty); - match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(Beatmap.Length); - match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(Beatmap.BPM); + match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarDifficulty); + match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.BaseDifficulty.ApproachRate); + match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.BaseDifficulty.DrainRate); + match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.BaseDifficulty.CircleSize); + match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.BaseDifficulty.OverallDifficulty); + match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length); + match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM); - match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(Beatmap.BeatDivisor); - match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(Beatmap.Status); + match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor); + match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(BeatmapInfo.Status); - match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(Beatmap.Metadata.AuthorString); - match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(Beatmap.Metadata.Artist) || - criteria.Artist.Matches(Beatmap.Metadata.ArtistUnicode); + match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.AuthorString); + match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || + criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); - match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(Beatmap.StarDifficulty); + match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarDifficulty); if (match) { - var terms = Beatmap.SearchableTerms; + var terms = BeatmapInfo.GetSearchableTerms(); foreach (var criteriaTerm in criteria.SearchTerms) match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)); @@ -66,16 +66,16 @@ namespace osu.Game.Screens.Select.Carousel // this should be done after text matching so we can prioritise matching numbers in metadata. if (!match && criteria.SearchNumber.HasValue) { - match = (Beatmap.OnlineBeatmapID == criteria.SearchNumber.Value) || - (Beatmap.BeatmapSet?.OnlineBeatmapSetID == criteria.SearchNumber.Value); + match = (BeatmapInfo.OnlineBeatmapID == criteria.SearchNumber.Value) || + (BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID == criteria.SearchNumber.Value); } } if (match) - match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true; + match &= criteria.Collection?.Beatmaps.Contains(BeatmapInfo) ?? true; if (match && criteria.RulesetCriteria != null) - match &= criteria.RulesetCriteria.Matches(Beatmap); + match &= criteria.RulesetCriteria.Matches(BeatmapInfo); Filtered.Value = !match; } @@ -89,13 +89,13 @@ namespace osu.Game.Screens.Select.Carousel { default: case SortMode.Difficulty: - var ruleset = Beatmap.RulesetID.CompareTo(otherBeatmap.Beatmap.RulesetID); + var ruleset = BeatmapInfo.RulesetID.CompareTo(otherBeatmap.BeatmapInfo.RulesetID); if (ruleset != 0) return ruleset; - return Beatmap.StarDifficulty.CompareTo(otherBeatmap.Beatmap.StarDifficulty); + return BeatmapInfo.StarDifficulty.CompareTo(otherBeatmap.BeatmapInfo.StarDifficulty); } } - public override string ToString() => Beatmap.ToString(); + public override string ToString() => BeatmapInfo.ToString(); } } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 00c2c2cb4a..0d7882bf17 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -47,8 +47,8 @@ namespace osu.Game.Screens.Select.Carousel { if (LastSelected == null || LastSelected.Filtered.Value) { - if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)) is BeatmapInfo recommended) - return Children.OfType().First(b => b.Beatmap == recommended); + if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.BeatmapInfo)) is BeatmapInfo recommended) + return Children.OfType().First(b => b.BeatmapInfo == recommended); } return base.GetNextToSelect(); @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select.Carousel /// /// All beatmaps which are not filtered and valid for display. /// - protected IEnumerable ValidBeatmaps => Beatmaps.Where(b => !b.Filtered.Value || b.State.Value == CarouselItemState.Selected).Select(b => b.Beatmap); + protected IEnumerable ValidBeatmaps => Beatmaps.Where(b => !b.Filtered.Value || b.State.Value == CarouselItemState.Selected).Select(b => b.BeatmapInfo); private int compareUsingAggregateMax(CarouselBeatmapSet other, Func func) { diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 633ef9297e..5940911d4a 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Select.Carousel private const float height = MAX_HEIGHT * 0.6f; - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private Sprite background; @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Select.Carousel public DrawableCarouselBeatmap(CarouselBeatmap panel) { - beatmap = panel.Beatmap; + beatmapInfo = panel.BeatmapInfo; Item = panel; } @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Select.Carousel Origin = Anchor.CentreLeft, Children = new Drawable[] { - new DifficultyIcon(beatmap, shouldShowTooltip: false) + new DifficultyIcon(beatmapInfo, shouldShowTooltip: false) { Scale = new Vector2(1.8f), }, @@ -129,7 +129,7 @@ namespace osu.Game.Screens.Select.Carousel { new OsuSpriteText { - Text = beatmap.Version, + Text = beatmapInfo.Version, Font = OsuFont.GetFont(size: 20), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Select.Carousel }, new OsuSpriteText { - Text = $"{(beatmap.Metadata ?? beatmap.BeatmapSet.Metadata).Author.Username}", + Text = $"{(beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet.Metadata).Author.Username}", Font = OsuFont.GetFont(italics: true), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft @@ -156,7 +156,7 @@ namespace osu.Game.Screens.Select.Carousel AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new TopLocalRank(beatmap) + new TopLocalRank(beatmapInfo) { Scale = new Vector2(0.8f), Size = new Vector2(40, 20) @@ -200,7 +200,7 @@ namespace osu.Game.Screens.Select.Carousel protected override bool OnClick(ClickEvent e) { if (Item.State.Value == CarouselItemState.Selected) - startRequested?.Invoke(beatmap); + startRequested?.Invoke(beatmapInfo); return base.OnClick(e); } @@ -216,7 +216,7 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value != CarouselItemState.Collapsed) { // We've potentially cancelled the computation above so a new bindable is required. - starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmapInfo, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); starDifficultyBindable.BindValueChanged(d => { starCounter.Current = (float)(d.NewValue?.Stars ?? 0); @@ -233,13 +233,13 @@ namespace osu.Game.Screens.Select.Carousel List items = new List(); if (startRequested != null) - items.Add(new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested(beatmap))); + items.Add(new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested(beatmapInfo))); if (editRequested != null) - items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmap))); + items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmapInfo))); - if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null) - items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); + if (beatmapInfo.OnlineBeatmapID.HasValue && beatmapOverlay != null) + items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineBeatmapID.Value))); if (collectionManager != null) { @@ -251,7 +251,7 @@ namespace osu.Game.Screens.Select.Carousel } if (hideRequested != null) - items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmap))); + items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmapInfo))); return items.ToArray(); } @@ -262,12 +262,12 @@ namespace osu.Game.Screens.Select.Carousel return new ToggleMenuItem(collection.Name.Value, MenuItemType.Standard, s => { if (s) - collection.Beatmaps.Add(beatmap); + collection.Beatmaps.Add(beatmapInfo); else - collection.Beatmaps.Remove(beatmap); + collection.Beatmaps.Remove(beatmapInfo); }) { - State = { Value = collection.Beatmaps.Contains(beatmap) } + State = { Value = collection.Beatmaps.Contains(beatmapInfo) } }; } diff --git a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs index 51fe7796c7..ce0cec837b 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Select.Carousel public readonly CarouselBeatmap Item; public FilterableDifficultyIcon(CarouselBeatmap item) - : base(item.Beatmap, performBackgroundDifficultyLookup: false) + : base(item.BeatmapInfo, performBackgroundDifficultyLookup: false) { filtered.BindTo(item.Filtered); filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); diff --git a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs index d2f9ed3a6a..acffdd9f64 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select.Carousel public readonly List Items; public FilterableGroupedDifficultyIcon(List items, RulesetInfo ruleset) - : base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White) + : base(items.Select(i => i.BeatmapInfo).ToList(), ruleset, Color4.White) { Items = items; diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 23a02547b2..9fb640ba1a 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select.Carousel var beatmaps = carouselSet.Beatmaps.ToList(); return beatmaps.Count > maximum_difficulty_icons - ? (IEnumerable)beatmaps.GroupBy(b => b.Beatmap.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key)) + ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key)) : beatmaps.Select(b => new FilterableDifficultyIcon(b)); } } diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 3ad57c1cb0..f2485587d8 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Select.Carousel { public class TopLocalRank : UpdateableRank { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; [Resolved] private ScoreManager scores { get; set; } @@ -31,10 +31,10 @@ namespace osu.Game.Screens.Select.Carousel private IBindable> itemUpdated; private IBindable> itemRemoved; - public TopLocalRank(BeatmapInfo beatmap) + public TopLocalRank(BeatmapInfo beatmapInfo) : base(null) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; } [BackgroundDependencyLoader] @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Select.Carousel { if (weakScore.NewValue.TryGetTarget(out var score)) { - if (score.BeatmapInfoID == beatmap.ID) + if (score.BeatmapInfoID == beatmapInfo.ID) fetchAndLoadTopScore(); } } @@ -79,10 +79,10 @@ namespace osu.Game.Screens.Select.Carousel private ScoreInfo fetchTopScore() { - if (scores == null || beatmap == null || ruleset?.Value == null || api?.LocalUser.Value == null) + if (scores == null || beatmapInfo == null || ruleset?.Value == null || api?.LocalUser.Value == null) return null; - return scores.QueryScores(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmap.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) + return scores.QueryScores(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmapInfo.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) .OrderByDescending(s => s.TotalScore) .FirstOrDefault(); } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 53e30fd9ca..562ebad9fe 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -38,16 +38,16 @@ namespace osu.Game.Screens.Select.Details protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate; private readonly StatisticRow starDifficulty; - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; updateStatistics(); } @@ -106,18 +106,18 @@ namespace osu.Game.Screens.Select.Details private void updateStatistics() { - BeatmapDifficulty baseDifficulty = Beatmap?.BaseDifficulty; + IBeatmapDifficultyInfo baseDifficulty = BeatmapInfo?.BaseDifficulty; BeatmapDifficulty adjustedDifficulty = null; if (baseDifficulty != null && mods.Value.Any(m => m is IApplicableToDifficulty)) { - adjustedDifficulty = baseDifficulty.Clone(); + adjustedDifficulty = new BeatmapDifficulty(baseDifficulty); foreach (var mod in mods.Value.OfType()) mod.ApplyToDifficulty(adjustedDifficulty); } - switch (Beatmap?.Ruleset?.ID ?? 0) + switch (BeatmapInfo?.Ruleset?.ID ?? 0) { case 3: // Account for mania differences locally for now @@ -145,13 +145,13 @@ namespace osu.Game.Screens.Select.Details { starDifficultyCancellationSource?.Cancel(); - if (Beatmap == null) + if (BeatmapInfo == null) return; starDifficultyCancellationSource = new CancellationTokenSource(); - var normalStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token); - var moddedStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); + var normalStarDifficulty = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, null, starDifficultyCancellationSource.Token); + var moddedStarDifficulty = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); Task.WhenAll(normalStarDifficulty, moddedStarDifficulty).ContinueWith(_ => Schedule(() => { diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 7820264505..07300635aa 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -25,17 +25,17 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private RulesetStore rulesets { get; set; } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (beatmap == value) + if (beatmapInfo == value) return; - beatmap = value; + beatmapInfo = value; Scores = null; UpdateScores(); @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (score.NewValue.TryGetTarget(out var scoreInfo)) { - if (Beatmap?.ID != scoreInfo.BeatmapInfoID) + if (BeatmapInfo?.ID != scoreInfo.BeatmapInfoID) return; } @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Select.Leaderboards loadCancellationSource?.Cancel(); loadCancellationSource = new CancellationTokenSource(); - if (Beatmap == null) + if (BeatmapInfo == null) { PlaceholderState = PlaceholderState.NoneSelected; return null; @@ -141,7 +141,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { var scores = scoreManager - .QueryScores(s => !s.DeletePending && s.Beatmap.ID == Beatmap.ID && s.Ruleset.ID == ruleset.Value.ID); + .QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == BeatmapInfo.ID && s.Ruleset.ID == ruleset.Value.ID); if (filterMods && !mods.Value.Any()) { @@ -168,7 +168,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (Beatmap.OnlineBeatmapID == null || Beatmap?.Status <= BeatmapSetOnlineStatus.Pending) + if (BeatmapInfo.OnlineBeatmapID == null || BeatmapInfo?.Status <= BeatmapSetOnlineStatus.Pending) { PlaceholderState = PlaceholderState.Unavailable; return null; @@ -188,7 +188,7 @@ namespace osu.Game.Screens.Select.Leaderboards else if (filterMods) requestMods = mods.Value; - var req = new GetScoresRequest(Beatmap, ruleset.Value ?? Beatmap.Ruleset, Scope, requestMods); + var req = new GetScoresRequest(BeatmapInfo, ruleset.Value ?? BeatmapInfo.Ruleset, Scope, requestMods); req.Success += r => { diff --git a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs index 085ea372c0..1ae244281b 100644 --- a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs +++ b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs @@ -29,8 +29,8 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader] private void load() { - BeatmapInfo beatmap = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID); - Debug.Assert(beatmap != null); + BeatmapInfo beatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID); + Debug.Assert(beatmapInfo != null); BodyText = $"{score.User} ({score.DisplayAccuracy}, {score.Rank})"; diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index c87a4bbc54..b8b8e3e4bc 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Select { base.Beatmap = value; - Leaderboard.Beatmap = value is DummyWorkingBeatmap ? null : value?.BeatmapInfo; + Leaderboard.BeatmapInfo = value is DummyWorkingBeatmap ? null : value?.BeatmapInfo; } } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 418cf23ce7..94aa165785 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Select } protected void PresentScore(ScoreInfo score) => - FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false))); + FinaliseSelection(score.BeatmapInfo, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false))); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9801098952..6cafcb9d16 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -345,22 +345,22 @@ namespace osu.Game.Screens.Select /// protected abstract BeatmapDetailArea CreateBeatmapDetailArea(); - public void Edit(BeatmapInfo beatmap = null) + public void Edit(BeatmapInfo beatmapInfo = null) { if (!AllowEditing) throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled"); - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo ?? beatmapInfoNoDebounce); this.Push(new EditorLoader()); } /// /// Call to make a selection and perform the default action for this SongSelect. /// - /// An optional beatmap to override the current carousel selection. + /// An optional beatmap to override the current carousel selection. /// An optional ruleset to override the current carousel selection. /// An optional custom action to perform instead of . - public void FinaliseSelection(BeatmapInfo beatmap = null, RulesetInfo ruleset = null, Action customStartAction = null) + public void FinaliseSelection(BeatmapInfo beatmapInfo = null, RulesetInfo ruleset = null, Action customStartAction = null) { // This is very important as we have not yet bound to screen-level bindables before the carousel load is completed. if (!Carousel.BeatmapSetsLoaded) @@ -377,10 +377,10 @@ namespace osu.Game.Screens.Select // avoid attempting to continue before a selection has been obtained. // this could happen via a user interaction while the carousel is still in a loading state. - if (Carousel.SelectedBeatmap == null) return; + if (Carousel.SelectedBeatmapInfo == null) return; - if (beatmap != null) - Carousel.SelectBeatmap(beatmap); + if (beatmapInfo != null) + Carousel.SelectBeatmap(beatmapInfo); if (selectionChangedDebounce?.Completed == false) { @@ -435,18 +435,18 @@ namespace osu.Game.Screens.Select } // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds. - private BeatmapInfo beatmapNoDebounce; + private BeatmapInfo beatmapInfoNoDebounce; private RulesetInfo rulesetNoDebounce; - private void updateSelectedBeatmap(BeatmapInfo beatmap) + private void updateSelectedBeatmap(BeatmapInfo beatmapInfo) { - if (beatmap == null && beatmapNoDebounce == null) + if (beatmapInfo == null && beatmapInfoNoDebounce == null) return; - if (beatmap?.Equals(beatmapNoDebounce) == true) + if (beatmapInfo?.Equals(beatmapInfoNoDebounce) == true) return; - beatmapNoDebounce = beatmap; + beatmapInfoNoDebounce = beatmapInfo; performUpdateSelected(); } @@ -467,12 +467,12 @@ namespace osu.Game.Screens.Select /// private void performUpdateSelected() { - var beatmap = beatmapNoDebounce; + var beatmap = beatmapInfoNoDebounce; var ruleset = rulesetNoDebounce; selectionChangedDebounce?.Cancel(); - if (beatmapNoDebounce == null) + if (beatmapInfoNoDebounce == null) run(); else selectionChangedDebounce = Scheduler.AddDelayed(run, 200); @@ -803,11 +803,11 @@ namespace osu.Game.Screens.Select dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); } - private void clearScores(BeatmapInfo beatmap) + private void clearScores(BeatmapInfo beatmapInfo) { - if (beatmap == null || beatmap.ID <= 0) return; + if (beatmapInfo == null || beatmapInfo.ID <= 0) return; - dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmap, () => + dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmapInfo, () => // schedule done here rather than inside the dialog as the dialog may fade out and never callback. Schedule(() => BeatmapDetails.Refresh()))); } diff --git a/osu.Game/Screens/Spectate/GameplayState.cs b/osu.Game/Screens/Spectate/SpectatorGameplayState.cs similarity index 81% rename from osu.Game/Screens/Spectate/GameplayState.cs rename to osu.Game/Screens/Spectate/SpectatorGameplayState.cs index 4579b9c07c..6ca1ac9a0a 100644 --- a/osu.Game/Screens/Spectate/GameplayState.cs +++ b/osu.Game/Screens/Spectate/SpectatorGameplayState.cs @@ -8,9 +8,9 @@ using osu.Game.Scoring; namespace osu.Game.Screens.Spectate { /// - /// The gameplay state of a spectated user. This class is immutable. + /// An immutable spectator gameplay state. /// - public class GameplayState + public class SpectatorGameplayState { /// /// The score which the user is playing. @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Spectate /// public readonly WorkingBeatmap Beatmap; - public GameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap) + public SpectatorGameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap) { Score = score; Ruleset = ruleset; diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index f0a68ea078..7861d4cb72 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Spectate private readonly IBindableDictionary playingUserStates = new BindableDictionary(); private readonly Dictionary userMap = new Dictionary(); - private readonly Dictionary gameplayStates = new Dictionary(); + private readonly Dictionary gameplayStates = new Dictionary(); private IBindable> managerUpdated; @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Spectate { ScoreInfo = new ScoreInfo { - Beatmap = resolvedBeatmap, + BeatmapInfo = resolvedBeatmap, User = user, Mods = spectatorState.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), Ruleset = resolvedRuleset.RulesetInfo, @@ -173,7 +173,7 @@ namespace osu.Game.Screens.Spectate Replay = new Replay { HasReceivedAllFrames = false }, }; - var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); + var gameplayState = new SpectatorGameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); gameplayStates[userId] = gameplayState; Schedule(() => StartGameplay(userId, gameplayState)); @@ -190,8 +190,8 @@ namespace osu.Game.Screens.Spectate /// Starts gameplay for a user. /// /// The user to start gameplay for. - /// The gameplay state. - protected abstract void StartGameplay(int userId, [NotNull] GameplayState gameplayState); + /// The gameplay state. + protected abstract void StartGameplay(int userId, [NotNull] SpectatorGameplayState spectatorGameplayState); /// /// Ends gameplay for a user. diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index d3adae5c8c..8e03bddb4d 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -68,6 +68,7 @@ namespace osu.Game.Skinning var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); + var ppCounter = container.OfType().FirstOrDefault(); if (score != null) { @@ -81,6 +82,13 @@ namespace osu.Game.Skinning score.Position = new Vector2(0, vertical_offset); + if (ppCounter != null) + { + ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4; + ppCounter.Origin = Anchor.TopCentre; + ppCounter.Anchor = Anchor.TopCentre; + } + if (accuracy != null) { accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5); @@ -123,6 +131,7 @@ namespace osu.Game.Skinning new SongProgress(), new BarHitErrorMeter(), new BarHitErrorMeter(), + new PerformancePointsCounter() } }; diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index e6ddeba316..2093182dcc 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -20,8 +20,8 @@ namespace osu.Game.Skinning protected override bool AllowManiaSkin => false; protected override bool UseCustomSampleBanks => true; - public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, IStorageResourceProvider resources) - : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), resources, beatmap.Path) + public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IResourceStore storage, IStorageResourceProvider resources) + : base(createSkinInfo(beatmapInfo), new LegacySkinResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; @@ -76,7 +76,7 @@ namespace osu.Game.Skinning return base.GetSample(sampleInfo); } - private static SkinInfo createSkinInfo(BeatmapInfo beatmap) => - new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata?.AuthorString }; + private static SkinInfo createSkinInfo(BeatmapInfo beatmapInfo) => + new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata?.AuthorString }; } } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index b6cb8fc7a4..92441f40da 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; +using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.IO; using osu.Game.Screens.Play.HUD; @@ -55,13 +56,20 @@ namespace osu.Game.Skinning if (bytes == null) continue; - string jsonContent = Encoding.UTF8.GetString(bytes); - var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); + try + { + string jsonContent = Encoding.UTF8.GetString(bytes); + var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); - if (deserializedContent == null) - continue; + if (deserializedContent == null) + continue; - DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray(); + DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray(); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to load skin configuration."); + } } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index edeb17cbad..3842acab74 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -207,7 +207,7 @@ namespace osu.Game.Skinning Name = skin.SkinInfo.Name + " (modified)", Creator = skin.SkinInfo.Creator, InstantiationInfo = skin.SkinInfo.InstantiationInfo, - }).Result; + }).Result.Value; } public void Save(Skin skin) diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs index c22ca0d8c0..66db965803 100644 --- a/osu.Game/Storyboards/CommandLoop.cs +++ b/osu.Game/Storyboards/CommandLoop.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; namespace osu.Game.Storyboards @@ -8,20 +9,31 @@ namespace osu.Game.Storyboards public class CommandLoop : CommandTimelineGroup { public double LoopStartTime; - public int LoopCount; + + /// + /// The total number of times this loop is played back. Always greater than zero. + /// + public readonly int TotalIterations; public override double StartTime => LoopStartTime + CommandsStartTime; - public override double EndTime => StartTime + CommandsDuration * LoopCount; + public override double EndTime => StartTime + CommandsDuration * TotalIterations; - public CommandLoop(double startTime, int loopCount) + /// + /// Construct a new command loop. + /// + /// The start time of the loop. + /// The number of times the loop should repeat. Should be greater than zero. Zero means a single playback. + public CommandLoop(double startTime, int repeatCount) { + if (repeatCount < 0) throw new ArgumentException("Repeat count must be zero or above.", nameof(repeatCount)); + LoopStartTime = startTime; - LoopCount = loopCount; + TotalIterations = repeatCount + 1; } public override IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) { - for (var loop = 0; loop < LoopCount; loop++) + for (var loop = 0; loop < TotalIterations; loop++) { var loopOffset = LoopStartTime + loop * CommandsDuration; foreach (var command in base.GetCommands(timelineSelector, offset + loopOffset)) @@ -30,6 +42,6 @@ namespace osu.Game.Storyboards } public override string ToString() - => $"{LoopStartTime} x{LoopCount}"; + => $"{LoopStartTime} x{TotalIterations}"; } } diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index bf87e7d10e..6fb2f5994b 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -1,13 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osuTK; -using osu.Framework.Graphics; -using osu.Game.Storyboards.Drawables; using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; +using osu.Framework.Graphics; +using osu.Game.Storyboards.Drawables; +using osuTK; namespace osu.Game.Storyboards { @@ -78,9 +78,9 @@ namespace osu.Game.Storyboards InitialPosition = initialPosition; } - public CommandLoop AddLoop(double startTime, int loopCount) + public CommandLoop AddLoop(double startTime, int repeatCount) { - var loop = new CommandLoop(startTime, loopCount); + var loop = new CommandLoop(startTime, repeatCount); loops.Add(loop); return loop; } diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index 27162b1d66..5c522058d9 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -111,8 +111,8 @@ namespace osu.Game.Tests.Beatmaps public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod; - public TestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) - : base(beatmap, new ResourceStore(), null) + public TestBeatmapSkin(BeatmapInfo beatmapInfo, bool hasColours) + : base(beatmapInfo, new ResourceStore(), null) { if (hasColours) { diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs index 5ce6aae647..719d31b092 100644 --- a/osu.Game/Tests/TestScoreInfo.cs +++ b/osu.Game/Tests/TestScoreInfo.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }; - Beatmap = new TestBeatmap(ruleset).BeatmapInfo; + BeatmapInfo = new TestBeatmap(ruleset).BeatmapInfo; Ruleset = ruleset; RulesetID = ruleset.ID ?? 0; diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 1e26036116..798b0d01ee 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -123,11 +123,40 @@ namespace osu.Game.Tests.Visual this.testBeatmap = testBeatmap; } - protected override string ComputeHash(BeatmapSetInfo item, ArchiveReader reader = null) - => string.Empty; + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) + { + return new TestBeatmapModelManager(storage, contextFactory, rulesets, api, host); + } - public override WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) - => testBeatmap; + protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) + { + return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); + } + + private class TestWorkingBeatmapCache : WorkingBeatmapCache + { + private readonly TestBeatmapManager testBeatmapManager; + + public TestWorkingBeatmapCache(TestBeatmapManager testBeatmapManager, AudioManager audioManager, IResourceStore resourceStore, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost gameHost) + : base(audioManager, resourceStore, storage, defaultBeatmap, gameHost) + { + this.testBeatmapManager = testBeatmapManager; + } + + public override WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) + => testBeatmapManager.testBeatmap; + } + + internal class TestBeatmapModelManager : BeatmapModelManager + { + public TestBeatmapModelManager(Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) + : base(storage, databaseContextFactory, rulesetStore, gameHost) + { + } + + protected override string ComputeHash(BeatmapSetInfo item, ArchiveReader reader = null) + => string.Empty; + } public override void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) { diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 881c4bab02..77db697cb6 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -22,6 +22,7 @@ using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Menu; using osuTK.Graphics; +using IntroSequence = osu.Game.Configuration.IntroSequence; namespace osu.Game.Tests.Visual { @@ -83,8 +84,17 @@ namespace osu.Game.Tests.Visual protected void PushAndConfirm(Func newScreen) { Screen screen = null; - AddStep("Push new screen", () => Game.ScreenStack.Push(screen = newScreen())); - AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen == screen && screen.IsLoaded); + IScreen previousScreen = null; + + AddStep("Push new screen", () => + { + previousScreen = Game.ScreenStack.CurrentScreen; + Game.ScreenStack.Push(screen = newScreen()); + }); + + AddUntilStep("Wait for new screen", () => screen.IsLoaded + && Game.ScreenStack.CurrentScreen != previousScreen + && previousScreen.GetChildScreen() == screen); } protected void ConfirmAtMainMenu() => AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded); @@ -117,7 +127,8 @@ namespace osu.Game.Tests.Visual 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(); @@ -133,6 +144,9 @@ namespace osu.Game.Tests.Visual 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/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index b30be05ac4..aa46b516bf 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Screens; @@ -32,6 +33,9 @@ namespace osu.Game.Tests.Visual content = new Container { RelativeSizeAxes = Axes.Both }, DialogOverlay = new DialogOverlay() }); + + Stack.ScreenPushed += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); + Stack.ScreenExited += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); } protected void LoadScreen(OsuScreen screen) => Stack.Push(screen); diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 5e5f20b307..d68984b144 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual if (autoplayMod != null) { - DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value)); + DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayState.Beatmap, Mods.Value)); return; } diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 75aa4866ff..91bcb37fcc 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -27,13 +27,13 @@ namespace osu.Game.Users public abstract class InGame : UserActivity { - public BeatmapInfo Beatmap { get; } + public BeatmapInfo BeatmapInfo { get; } public RulesetInfo Ruleset { get; } - protected InGame(BeatmapInfo info, RulesetInfo ruleset) + protected InGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) { - Beatmap = info; + BeatmapInfo = beatmapInfo; Ruleset = ruleset; } @@ -42,8 +42,8 @@ namespace osu.Game.Users public class InMultiplayerGame : InGame { - public InMultiplayerGame(BeatmapInfo beatmap, RulesetInfo ruleset) - : base(beatmap, ruleset) + public InMultiplayerGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) + : base(beatmapInfo, ruleset) { } @@ -52,27 +52,27 @@ namespace osu.Game.Users public class InPlaylistGame : InGame { - public InPlaylistGame(BeatmapInfo beatmap, RulesetInfo ruleset) - : base(beatmap, ruleset) + public InPlaylistGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) + : base(beatmapInfo, ruleset) { } } public class InSoloGame : InGame { - public InSoloGame(BeatmapInfo info, RulesetInfo ruleset) - : base(info, ruleset) + public InSoloGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) + : base(beatmapInfo, ruleset) { } } public class Editing : UserActivity { - public BeatmapInfo Beatmap { get; } + public BeatmapInfo BeatmapInfo { get; } public Editing(BeatmapInfo info) { - Beatmap = info; + BeatmapInfo = info; } public override string Status => @"Editing a beatmap"; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba118c5240..4877ddf725 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,12 +20,12 @@ - + - - - - + + + + @@ -35,10 +35,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + diff --git a/osu.iOS.props b/osu.iOS.props index 37931d0c38..edce9d27fe 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,8 +70,8 @@ - - + + @@ -93,12 +93,12 @@ - + - +