From 83cb70db17e10ee9c42ba5c20e5547efff6a9118 Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Mon, 19 Aug 2019 22:54:07 +0200 Subject: [PATCH 001/587] Added initial AimAssist mod --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 97 +++++++++++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 1 + 2 files changed, 98 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs new file mode 100644 index 0000000000..325c5d0c13 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -0,0 +1,97 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Osu.Mods +{ + internal class OsuModAimAssist : Mod, IApplicableToDrawableHitObjects, IUpdatableByPlayfield + { + public override string Name => "AimAssist"; + public override string Acronym => "AA"; + public override IconUsage Icon => FontAwesome.Solid.MousePointer; + public override ModType Type => ModType.Fun; + public override string Description => "No need to chase the circle, the circle chases you"; + public override double ScoreMultiplier => 1; + public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModAutoplay), typeof(OsuModWiggle), typeof(OsuModTransform) }; + + private HashSet movingObjects = new HashSet(); + private int updateCounter = 0; + + public void Update(Playfield playfield) + { + // Avoid crowded judgment displays + playfield.DisplayJudgements.Value = false; + + // Object destination updated when cursor updates + playfield.Cursor.ActiveCursor.OnUpdate += drawableCursor => + { + // ... every 500th cursor update iteration + // (lower -> potential lags ; higher -> easier to miss if cursor too fast) + if (updateCounter++ < 500) return; + updateCounter = 0; + + // First move objects to new destination, then remove them from movingObjects set if they're too old + movingObjects.RemoveWhere(d => + { + var currentTime = playfield.Clock.CurrentTime; + var h = d.HitObject; + d.ClearTransforms(); + switch (d) + { + case DrawableHitCircle circle: + d.MoveTo(drawableCursor.DrawPosition, Math.Max(0, h.StartTime - currentTime)); + return currentTime > h.StartTime; + case DrawableSlider slider: + + // Move slider to cursor + if (currentTime < h.StartTime) + d.MoveTo(drawableCursor.DrawPosition, Math.Max(0, h.StartTime - currentTime)); + + // Move slider so that sliderball stays on the cursor + else + d.MoveTo(drawableCursor.DrawPosition - slider.Ball.DrawPosition, Math.Max(0, h.StartTime - currentTime)); + return currentTime > (h as IHasEndTime).EndTime - 50; + case DrawableSpinner spinner: + // TODO + return true; + } + return true; // never happens(?) + }); + }; + } + + public void ApplyToDrawableHitObjects(IEnumerable drawables) + { + foreach (var drawable in drawables) + drawable.ApplyCustomUpdateState += drawableOnApplyCustomUpdateState; + } + + private void drawableOnApplyCustomUpdateState(DrawableHitObject drawable, ArmedState state) + { + if (!(drawable is DrawableOsuHitObject d)) + return; + var h = d.HitObject; + using (d.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) + movingObjects.Add(d); + } + } + + /* + * TODOs + * - remove object timing glitches / artifacts + * - remove FollowPoints + * - automate sliders + * - combine with OsuModRelax (?) + * - must be some way to make this more effictient + * + */ +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index d50d4f401c..9dbc144501 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -136,6 +136,7 @@ namespace osu.Game.Rulesets.Osu new OsuModSpinIn(), new MultiMod(new OsuModGrow(), new OsuModDeflate()), new MultiMod(new ModWindUp(), new ModWindDown()), + new OsuModAimAssist(), }; case ModType.System: From 3434458e0a2119e1ee3171b96186f1b2cc4a0a53 Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Mon, 19 Aug 2019 23:51:47 +0200 Subject: [PATCH 002/587] Minor formatting changes --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index 325c5d0c13..26f0d64a5d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -23,8 +23,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModAutoplay), typeof(OsuModWiggle), typeof(OsuModTransform) }; - private HashSet movingObjects = new HashSet(); - private int updateCounter = 0; + private readonly HashSet movingObjects = new HashSet(); + private int updateCounter; public void Update(Playfield playfield) { @@ -36,7 +36,9 @@ namespace osu.Game.Rulesets.Osu.Mods { // ... every 500th cursor update iteration // (lower -> potential lags ; higher -> easier to miss if cursor too fast) - if (updateCounter++ < 500) return; + if (updateCounter++ < 500) + return; + updateCounter = 0; // First move objects to new destination, then remove them from movingObjects set if they're too old @@ -45,11 +47,13 @@ namespace osu.Game.Rulesets.Osu.Mods var currentTime = playfield.Clock.CurrentTime; var h = d.HitObject; d.ClearTransforms(); + switch (d) { case DrawableHitCircle circle: - d.MoveTo(drawableCursor.DrawPosition, Math.Max(0, h.StartTime - currentTime)); + circle.MoveTo(drawableCursor.DrawPosition, Math.Max(0, h.StartTime - currentTime)); return currentTime > h.StartTime; + case DrawableSlider slider: // Move slider to cursor @@ -59,11 +63,13 @@ namespace osu.Game.Rulesets.Osu.Mods // Move slider so that sliderball stays on the cursor else d.MoveTo(drawableCursor.DrawPosition - slider.Ball.DrawPosition, Math.Max(0, h.StartTime - currentTime)); - return currentTime > (h as IHasEndTime).EndTime - 50; - case DrawableSpinner spinner: + return currentTime > (h as IHasEndTime)?.EndTime - 50; + + case DrawableSpinner _: // TODO return true; } + return true; // never happens(?) }); }; @@ -79,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.Mods { if (!(drawable is DrawableOsuHitObject d)) return; + var h = d.HitObject; using (d.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) movingObjects.Add(d); From 28f78f67b237a952bb403fb802640b7c4d8c47b3 Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Tue, 20 Aug 2019 16:33:56 +0200 Subject: [PATCH 003/587] No longer subscribing to OnUpdate --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 70 ++++++++----------- 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index 26f0d64a5d..8d0c1fdcfe 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods { internal class OsuModAimAssist : Mod, IApplicableToDrawableHitObjects, IUpdatableByPlayfield { - public override string Name => "AimAssist"; + public override string Name => "Aim Assist"; public override string Acronym => "AA"; public override IconUsage Icon => FontAwesome.Solid.MousePointer; public override ModType Type => ModType.Fun; @@ -24,55 +24,45 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModAutoplay), typeof(OsuModWiggle), typeof(OsuModTransform) }; private readonly HashSet movingObjects = new HashSet(); - private int updateCounter; public void Update(Playfield playfield) { + var drawableCursor = playfield.Cursor.ActiveCursor; + // Avoid crowded judgment displays playfield.DisplayJudgements.Value = false; - // Object destination updated when cursor updates - playfield.Cursor.ActiveCursor.OnUpdate += drawableCursor => + // First move objects to new destination, then remove them from movingObjects set if they're too old + movingObjects.RemoveWhere(d => { - // ... every 500th cursor update iteration - // (lower -> potential lags ; higher -> easier to miss if cursor too fast) - if (updateCounter++ < 500) - return; + var currentTime = playfield.Clock.CurrentTime; + var h = d.HitObject; - updateCounter = 0; - - // First move objects to new destination, then remove them from movingObjects set if they're too old - movingObjects.RemoveWhere(d => + switch (d) { - var currentTime = playfield.Clock.CurrentTime; - var h = d.HitObject; - d.ClearTransforms(); + case DrawableHitCircle circle: + circle.MoveTo(drawableCursor.DrawPosition, Math.Max(0, h.StartTime - currentTime - 10)); + return currentTime > h.StartTime; - switch (d) - { - case DrawableHitCircle circle: - circle.MoveTo(drawableCursor.DrawPosition, Math.Max(0, h.StartTime - currentTime)); - return currentTime > h.StartTime; + case DrawableSlider slider: - case DrawableSlider slider: + // Move slider to cursor + if (currentTime < h.StartTime) + d.MoveTo(drawableCursor.DrawPosition, Math.Max(0, h.StartTime - currentTime - 10)); - // Move slider to cursor - if (currentTime < h.StartTime) - d.MoveTo(drawableCursor.DrawPosition, Math.Max(0, h.StartTime - currentTime)); + // Move slider so that sliderball stays on the cursor + else + d.MoveTo(drawableCursor.DrawPosition - slider.Ball.DrawPosition); + return currentTime > (h as IHasEndTime)?.EndTime; - // Move slider so that sliderball stays on the cursor - else - d.MoveTo(drawableCursor.DrawPosition - slider.Ball.DrawPosition, Math.Max(0, h.StartTime - currentTime)); - return currentTime > (h as IHasEndTime)?.EndTime - 50; + case DrawableSpinner _: + // TODO + return true; - case DrawableSpinner _: - // TODO - return true; - } - - return true; // never happens(?) - }); - }; + default: + return true; + } + }); } public void ApplyToDrawableHitObjects(IEnumerable drawables) @@ -83,11 +73,7 @@ namespace osu.Game.Rulesets.Osu.Mods private void drawableOnApplyCustomUpdateState(DrawableHitObject drawable, ArmedState state) { - if (!(drawable is DrawableOsuHitObject d)) - return; - - var h = d.HitObject; - using (d.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) + if (drawable is DrawableOsuHitObject d) movingObjects.Add(d); } } @@ -96,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Mods * TODOs * - remove object timing glitches / artifacts * - remove FollowPoints - * - automate sliders + * - automate spinners * - combine with OsuModRelax (?) * - must be some way to make this more effictient * From 30f923edde9987bfcfbe866f9bf5258dfdb5b8bf Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Tue, 20 Aug 2019 17:23:50 +0200 Subject: [PATCH 004/587] Hiding follow points --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 6 +++--- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index 8d0c1fdcfe..54c80525bf 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.Osu.UI; namespace osu.Game.Rulesets.Osu.Mods { @@ -29,8 +30,9 @@ namespace osu.Game.Rulesets.Osu.Mods { var drawableCursor = playfield.Cursor.ActiveCursor; - // Avoid crowded judgment displays + // Avoid crowded judgment displays and hide follow points playfield.DisplayJudgements.Value = false; + (playfield as OsuPlayfield)?.ConnectionLayer.Hide(); // First move objects to new destination, then remove them from movingObjects set if they're too old movingObjects.RemoveWhere(d => @@ -81,10 +83,8 @@ namespace osu.Game.Rulesets.Osu.Mods /* * TODOs * - remove object timing glitches / artifacts - * - remove FollowPoints * - automate spinners * - combine with OsuModRelax (?) - * - must be some way to make this more effictient * */ } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 9037faf606..64620d3629 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI { private readonly ApproachCircleProxyContainer approachCircles; private readonly JudgementContainer judgementLayer; - private readonly ConnectionRenderer connectionLayer; + public readonly ConnectionRenderer ConnectionLayer; public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.UI { InternalChildren = new Drawable[] { - connectionLayer = new FollowPointRenderer + ConnectionLayer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both, Depth = 2, @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.UI public override void PostProcess() { - connectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType(); + ConnectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType(); } private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) From d4d348390a6ce2dfac7638d61b8800e5439dceaf Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Tue, 20 Aug 2019 23:18:57 +0200 Subject: [PATCH 005/587] Change set to list + minor changes --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index 54c80525bf..1101b93ed1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -24,38 +24,46 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModAutoplay), typeof(OsuModWiggle), typeof(OsuModTransform) }; - private readonly HashSet movingObjects = new HashSet(); + private readonly List movingObjects = new List(); public void Update(Playfield playfield) { - var drawableCursor = playfield.Cursor.ActiveCursor; + var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; - // Avoid crowded judgment displays and hide follow points + // Avoid relocating judgment displays and hide follow points playfield.DisplayJudgements.Value = false; (playfield as OsuPlayfield)?.ConnectionLayer.Hide(); - // First move objects to new destination, then remove them from movingObjects set if they're too old - movingObjects.RemoveWhere(d => + // First move objects to new destination, then remove them from movingObjects list if they're too old + movingObjects.RemoveAll(d => { - var currentTime = playfield.Clock.CurrentTime; var h = d.HitObject; + var currentTime = playfield.Clock.CurrentTime; + var endTime = (h as IHasEndTime)?.EndTime ?? h.StartTime; + d.ClearTransforms(); switch (d) { case DrawableHitCircle circle: - circle.MoveTo(drawableCursor.DrawPosition, Math.Max(0, h.StartTime - currentTime - 10)); - return currentTime > h.StartTime; + + // 10ms earlier on the note to reduce chance of missing when clicking early / cursor moves fast + circle.MoveTo(cursorPos, Math.Max(0, endTime - currentTime - 10)); + return currentTime > endTime; case DrawableSlider slider: // Move slider to cursor if (currentTime < h.StartTime) - d.MoveTo(drawableCursor.DrawPosition, Math.Max(0, h.StartTime - currentTime - 10)); - + { + slider.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); + return false; + } // Move slider so that sliderball stays on the cursor else - d.MoveTo(drawableCursor.DrawPosition - slider.Ball.DrawPosition); - return currentTime > (h as IHasEndTime)?.EndTime; + { + slider.MoveTo(cursorPos - slider.Ball.DrawPosition); + return currentTime > endTime; + } case DrawableSpinner _: // TODO @@ -75,14 +83,15 @@ namespace osu.Game.Rulesets.Osu.Mods private void drawableOnApplyCustomUpdateState(DrawableHitObject drawable, ArmedState state) { - if (drawable is DrawableOsuHitObject d) - movingObjects.Add(d); + if (drawable is DrawableOsuHitObject hitobject) + movingObjects.Add(hitobject); } } /* * TODOs - * - remove object timing glitches / artifacts + * - fix sliders reappearing at original position after their EndTime (see https://puu.sh/E7zT4/111cf9cdc8.gif) + * - relocate / hide slider headcircle's explosion, flash, ... * - automate spinners * - combine with OsuModRelax (?) * From 4cd5eb783a017f53682291aaa61eb32e91b72bd9 Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Wed, 21 Aug 2019 21:37:56 +0200 Subject: [PATCH 006/587] Add spinner automation --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 64 +++++++++++++++---- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 2 +- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index 1101b93ed1..f70ad2ac7c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Osu.UI; +using osuTK; namespace osu.Game.Rulesets.Osu.Mods { @@ -22,25 +23,30 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override string Description => "No need to chase the circle, the circle chases you"; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModAutoplay), typeof(OsuModWiggle), typeof(OsuModTransform) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) }; private readonly List movingObjects = new List(); + private DrawableSpinner activeSpinner; + private double spinnerAngle; // in radians public void Update(Playfield playfield) { var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; + var currentTime = playfield.Clock.CurrentTime; // Avoid relocating judgment displays and hide follow points playfield.DisplayJudgements.Value = false; (playfield as OsuPlayfield)?.ConnectionLayer.Hide(); - // First move objects to new destination, then remove them from movingObjects list if they're too old + // If object too old, remove from movingObjects list, otherwise move to new destination movingObjects.RemoveAll(d => { var h = d.HitObject; - var currentTime = playfield.Clock.CurrentTime; var endTime = (h as IHasEndTime)?.EndTime ?? h.StartTime; - d.ClearTransforms(); + + // Object no longer required to be moved -> remove from list + if (currentTime > endTime) + return true; switch (d) { @@ -48,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Mods // 10ms earlier on the note to reduce chance of missing when clicking early / cursor moves fast circle.MoveTo(cursorPos, Math.Max(0, endTime - currentTime - 10)); - return currentTime > endTime; + return false; case DrawableSlider slider: @@ -56,23 +62,56 @@ namespace osu.Game.Rulesets.Osu.Mods if (currentTime < h.StartTime) { slider.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); - return false; } // Move slider so that sliderball stays on the cursor else { + slider.HeadCircle.Hide(); // temporary solution to supress HeadCircle's explosion, flash, ... at wrong location slider.MoveTo(cursorPos - slider.Ball.DrawPosition); - return currentTime > endTime; } - case DrawableSpinner _: - // TODO - return true; + return false; + + case DrawableSpinner spinner: + + // Move spinner to cursor + if (currentTime < h.StartTime) + { + spinner.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); + return false; + } + else + { + spinnerAngle = 0; + activeSpinner = spinner; + return true; + } default: return true; } }); + + if (activeSpinner != null) + { + if (currentTime > (activeSpinner.HitObject as IHasEndTime)?.EndTime) + { + activeSpinner = null; + spinnerAngle = 0; + } + else + { + const float additional_degrees = 4; + const int dist_from_cursor = 30; + spinnerAngle += additional_degrees * Math.PI / 180; + + // Visual progress + activeSpinner.MoveTo(new Vector2((float)(dist_from_cursor * Math.Cos(spinnerAngle) + cursorPos.X), (float)(dist_from_cursor * Math.Sin(spinnerAngle) + cursorPos.Y))); + + // Logical progress + activeSpinner.Disc.RotationAbsolute += additional_degrees; + } + } } public void ApplyToDrawableHitObjects(IEnumerable drawables) @@ -91,9 +130,8 @@ namespace osu.Game.Rulesets.Osu.Mods /* * TODOs * - fix sliders reappearing at original position after their EndTime (see https://puu.sh/E7zT4/111cf9cdc8.gif) - * - relocate / hide slider headcircle's explosion, flash, ... - * - automate spinners - * - combine with OsuModRelax (?) + * - find nicer way to handle slider headcircle explosion, flash, ... + * - add Aim Assist as incompatible mod for Autoplay (?) * */ } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index ca72f18e9c..0757547d1c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Automation; public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModAimAssist) }; public bool AllowFail => false; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 9b079895fa..7799cdac32 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override string Description => "Everything rotates. EVERYTHING."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModAimAssist) }; private float theta; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index 17fcd03dd5..a14fd88ff5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override string Description => "They just won't stay still..."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModAimAssist) }; private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles private const int wiggle_strength = 10; // Higher = stronger wiggles From 327822de5bb5e15098c53fce05ae0eddad495e58 Mon Sep 17 00:00:00 2001 From: pikokr Date: Mon, 27 Dec 2021 19:41:36 +0900 Subject: [PATCH 007/587] Add touchscreen support for osu!mania ruleset --- osu.Game.Rulesets.Mania/UI/Column.cs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 9d060944cd..df39b5397b 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -136,6 +136,33 @@ namespace osu.Game.Rulesets.Mania.UI { } + // https://github.com/ppy/osu-framework/blob/49c954321c3686628b2c223670363438f88a0341/osu.Framework/Graphics/Drawable.cs#L1513-L1524 + private T findClosestParent() where T : class, IDrawable + { + Drawable cursor = this; + + while ((cursor = cursor.Parent) != null) + { + if (cursor is T match) + return match; + } + + return default; + } + + private ManiaInputManager.RulesetKeyBindingContainer keyBindingManager => findClosestParent(); + + protected override bool OnTouchDown(TouchDownEvent e) + { + keyBindingManager.TriggerPressed(Action.Value); + return base.OnTouchDown(e); + } + + protected override void OnTouchUp(TouchUpEvent e) + { + keyBindingManager.TriggerReleased(Action.Value); + } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); From 58994b790c8e8a3c85fa5d82edc2835b648b8843 Mon Sep 17 00:00:00 2001 From: pikokr Date: Mon, 27 Dec 2021 21:20:52 +0900 Subject: [PATCH 008/587] Get key binding container once instead of getting on every touch --- osu.Game.Rulesets.Mania/ManiaInputManager.cs | 5 +++++ osu.Game.Rulesets.Mania/UI/Column.cs | 21 ++++++-------------- osu.Game/Rulesets/UI/DrawableRuleset.cs | 1 + 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index 186fc4b15d..bf94735077 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Mania : base(ruleset, variant, SimultaneousBindingMode.Unique) { } + + public RulesetKeyBindingContainer GetKeyBindingContainer() + { + return (RulesetKeyBindingContainer)KeyBindingContainer; + } } public enum ManiaAction diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index df39b5397b..6be36619c7 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -136,31 +136,22 @@ namespace osu.Game.Rulesets.Mania.UI { } - // https://github.com/ppy/osu-framework/blob/49c954321c3686628b2c223670363438f88a0341/osu.Framework/Graphics/Drawable.cs#L1513-L1524 - private T findClosestParent() where T : class, IDrawable + private ManiaInputManager.RulesetKeyBindingContainer keyBindingContainer { get; set; } + + private ManiaInputManager.RulesetKeyBindingContainer getKeyBindingManager() { - Drawable cursor = this; - - while ((cursor = cursor.Parent) != null) - { - if (cursor is T match) - return match; - } - - return default; + return keyBindingContainer ??= ((ManiaInputManager)GetContainingInputManager()).GetKeyBindingContainer(); } - private ManiaInputManager.RulesetKeyBindingContainer keyBindingManager => findClosestParent(); - protected override bool OnTouchDown(TouchDownEvent e) { - keyBindingManager.TriggerPressed(Action.Value); + getKeyBindingManager().TriggerPressed(Action.Value); return base.OnTouchDown(e); } protected override void OnTouchUp(TouchUpEvent e) { - keyBindingManager.TriggerReleased(Action.Value); + getKeyBindingManager().TriggerReleased(Action.Value); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 29559f5036..30012784a3 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -50,6 +50,7 @@ namespace osu.Game.Rulesets.UI /// /// The key conversion input manager for this DrawableRuleset. /// + [Cached] public PassThroughInputManager KeyBindingInputManager; public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0; From d62930500298d23419b2f37b0376881f0e6bb573 Mon Sep 17 00:00:00 2001 From: pikokr Date: Tue, 28 Dec 2021 21:47:58 +0900 Subject: [PATCH 009/587] Remove `Cached` attribute from `DrawableRuleset.KeyBindingInputManager` --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 30012784a3..29559f5036 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -50,7 +50,6 @@ namespace osu.Game.Rulesets.UI /// /// The key conversion input manager for this DrawableRuleset. /// - [Cached] public PassThroughInputManager KeyBindingInputManager; public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0; From 59b4aea5f9dc22a3cf543bddae589e462a8be383 Mon Sep 17 00:00:00 2001 From: pikokr Date: Tue, 28 Dec 2021 21:52:46 +0900 Subject: [PATCH 010/587] Make method and property name to match class name --- osu.Game.Rulesets.Mania/UI/Column.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 6be36619c7..36473aa900 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -136,22 +136,22 @@ namespace osu.Game.Rulesets.Mania.UI { } - private ManiaInputManager.RulesetKeyBindingContainer keyBindingContainer { get; set; } + private ManiaInputManager.RulesetKeyBindingContainer rulesetKeyBindingContainer { get; set; } - private ManiaInputManager.RulesetKeyBindingContainer getKeyBindingManager() + private ManiaInputManager.RulesetKeyBindingContainer getRulesetKeyBindingContainer() { - return keyBindingContainer ??= ((ManiaInputManager)GetContainingInputManager()).GetKeyBindingContainer(); + return rulesetKeyBindingContainer ??= ((ManiaInputManager)GetContainingInputManager()).GetKeyBindingContainer(); } protected override bool OnTouchDown(TouchDownEvent e) { - getKeyBindingManager().TriggerPressed(Action.Value); + getRulesetKeyBindingContainer().TriggerPressed(Action.Value); return base.OnTouchDown(e); } protected override void OnTouchUp(TouchUpEvent e) { - getKeyBindingManager().TriggerReleased(Action.Value); + getRulesetKeyBindingContainer().TriggerReleased(Action.Value); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) From 62d6bb8c2e9c8a6e10a318599a7ad04974d55e76 Mon Sep 17 00:00:00 2001 From: pikokr Date: Tue, 28 Dec 2021 22:35:45 +0900 Subject: [PATCH 011/587] Trigger touch on click key area --- osu.Game.Rulesets.Mania/UI/Column.cs | 18 ------------- .../UI/Components/DefaultKeyArea.cs | 25 +++++++++++++++++-- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 36473aa900..9d060944cd 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -136,24 +136,6 @@ namespace osu.Game.Rulesets.Mania.UI { } - private ManiaInputManager.RulesetKeyBindingContainer rulesetKeyBindingContainer { get; set; } - - private ManiaInputManager.RulesetKeyBindingContainer getRulesetKeyBindingContainer() - { - return rulesetKeyBindingContainer ??= ((ManiaInputManager)GetContainingInputManager()).GetKeyBindingContainer(); - } - - protected override bool OnTouchDown(TouchDownEvent e) - { - getRulesetKeyBindingContainer().TriggerPressed(Action.Value); - return base.OnTouchDown(e); - } - - protected override void OnTouchUp(TouchUpEvent e) - { - getRulesetKeyBindingContainer().TriggerReleased(Action.Value); - } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs index 267ed1f5f4..4e0bb71514 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs @@ -36,10 +36,31 @@ namespace osu.Game.Rulesets.Mania.UI.Components RelativeSizeAxes = Axes.Both; } + public class ChildContainer : Container + { + private ManiaInputManager.RulesetKeyBindingContainer keyBindingContainer { get; set; } + + private ManiaInputManager.RulesetKeyBindingContainer getKeyBindingContainer() + { + return keyBindingContainer ??= ((ManiaInputManager)GetContainingInputManager()).GetKeyBindingContainer(); + } + + protected override bool OnTouchDown(TouchDownEvent e) + { + getKeyBindingContainer().TriggerPressed(((DefaultKeyArea)Parent).column.Action.Value); + return base.OnTouchDown(e); + } + + protected override void OnTouchUp(TouchUpEvent e) + { + getKeyBindingContainer().TriggerReleased(((DefaultKeyArea)Parent).column.Action.Value); + } + } + [BackgroundDependencyLoader] private void load(IScrollingInfo scrollingInfo) { - InternalChild = directionContainer = new Container + InternalChild = directionContainer = new ChildContainer { RelativeSizeAxes = Axes.X, Height = Stage.HIT_TARGET_POSITION, @@ -69,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components AlwaysPresent = true } } - } + }, } }; From dec1f317494c73bbb4313d7be2f1cadb5fde488c Mon Sep 17 00:00:00 2001 From: pikokr Date: Tue, 28 Dec 2021 22:43:07 +0900 Subject: [PATCH 012/587] Make `KeyBindingContainer` public --- osu.Game.Rulesets.Mania/ManiaInputManager.cs | 5 ----- osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs | 2 +- osu.Game/Rulesets/UI/RulesetInputManager.cs | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index bf94735077..186fc4b15d 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -13,11 +13,6 @@ namespace osu.Game.Rulesets.Mania : base(ruleset, variant, SimultaneousBindingMode.Unique) { } - - public RulesetKeyBindingContainer GetKeyBindingContainer() - { - return (RulesetKeyBindingContainer)KeyBindingContainer; - } } public enum ManiaAction diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs index 4e0bb71514..b3a2a97bf7 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components private ManiaInputManager.RulesetKeyBindingContainer getKeyBindingContainer() { - return keyBindingContainer ??= ((ManiaInputManager)GetContainingInputManager()).GetKeyBindingContainer(); + return keyBindingContainer ??= (ManiaInputManager.RulesetKeyBindingContainer)((ManiaInputManager)GetContainingInputManager()).KeyBindingContainer; } protected override bool OnTouchDown(TouchDownEvent e) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 6564ff9e23..6ef99f8aae 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.UI protected override InputState CreateInitialState() => new RulesetInputManagerInputState(base.CreateInitialState()); - protected readonly KeyBindingContainer KeyBindingContainer; + public readonly KeyBindingContainer KeyBindingContainer; protected override Container Content => content; From 4cb8272d14369fb7a129d5ec3997b92a14e14867 Mon Sep 17 00:00:00 2001 From: pikokr Date: Thu, 30 Dec 2021 17:37:14 +0900 Subject: [PATCH 013/587] Column Touch area & highlighting on start --- osu.Game.Rulesets.Mania/UI/Column.cs | 60 ++++++++++++++++++- .../UI/Components/DefaultKeyArea.cs | 23 +------ 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 9d060944cd..1e07c84d9f 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -44,6 +44,63 @@ namespace osu.Game.Rulesets.Mania.UI private readonly GameplaySampleTriggerSource sampleTriggerSource; + public class ColumnTouchInputArea : Container + { + private Column column => (Column)Parent; + + private Container hintContainer; + + public ColumnTouchInputArea() + { + RelativeSizeAxes = Axes.X; + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + Height = 100; + InternalChild = hintContainer = new Container + { + RelativeSizeAxes = Axes.Both, + BorderColour = Color4.Red, + BorderThickness = 5, + Masking = true, + }; + } + + protected override void LoadComplete() + { + hintContainer.Delay(1000).FadeOutFromOne(500, Easing.OutSine); + } + + private ManiaInputManager.RulesetKeyBindingContainer keyBindingContainer { get; set; } + + private ManiaInputManager.RulesetKeyBindingContainer getKeyBindingContainer() + { + return keyBindingContainer ??= (ManiaInputManager.RulesetKeyBindingContainer)((ManiaInputManager)GetContainingInputManager()).KeyBindingContainer; + } + + protected override bool OnTouchDown(TouchDownEvent e) + { + getKeyBindingContainer().TriggerPressed(column.Action.Value); + return base.OnTouchDown(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + getKeyBindingContainer().TriggerPressed(column.Action.Value); + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + getKeyBindingContainer().TriggerReleased(column.Action.Value); + } + + protected override void OnTouchUp(TouchUpEvent e) + { + getKeyBindingContainer().TriggerReleased(column.Action.Value); + } + } + public Column(int index) { Index = index; @@ -68,7 +125,8 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Both }, background, - TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } + TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }, + new ColumnTouchInputArea() }; hitPolicy = new OrderedHitPolicy(HitObjectContainer); diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs index b3a2a97bf7..15018b464f 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs @@ -36,31 +36,10 @@ namespace osu.Game.Rulesets.Mania.UI.Components RelativeSizeAxes = Axes.Both; } - public class ChildContainer : Container - { - private ManiaInputManager.RulesetKeyBindingContainer keyBindingContainer { get; set; } - - private ManiaInputManager.RulesetKeyBindingContainer getKeyBindingContainer() - { - return keyBindingContainer ??= (ManiaInputManager.RulesetKeyBindingContainer)((ManiaInputManager)GetContainingInputManager()).KeyBindingContainer; - } - - protected override bool OnTouchDown(TouchDownEvent e) - { - getKeyBindingContainer().TriggerPressed(((DefaultKeyArea)Parent).column.Action.Value); - return base.OnTouchDown(e); - } - - protected override void OnTouchUp(TouchUpEvent e) - { - getKeyBindingContainer().TriggerReleased(((DefaultKeyArea)Parent).column.Action.Value); - } - } - [BackgroundDependencyLoader] private void load(IScrollingInfo scrollingInfo) { - InternalChild = directionContainer = new ChildContainer + InternalChild = directionContainer = new Container { RelativeSizeAxes = Axes.X, Height = Stage.HIT_TARGET_POSITION, From 612f69782b14e6d0397c9fd248b9f77ffd48e01e Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Tue, 4 Jan 2022 14:29:44 +0100 Subject: [PATCH 014/587] use Playfield.HitObjectContainer.AliveObjects --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 93 ++++++++++--------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index f70ad2ac7c..1849b48073 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -7,54 +7,55 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Osu.UI; using osuTK; +using System.Linq; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModAimAssist : Mod, IApplicableToDrawableHitObjects, IUpdatableByPlayfield + internal class OsuModAimAssist : Mod, IUpdatableByPlayfield { public override string Name => "Aim Assist"; public override string Acronym => "AA"; - public override IconUsage Icon => FontAwesome.Solid.MousePointer; + public override IconUsage? Icon => FontAwesome.Solid.MousePointer; public override ModType Type => ModType.Fun; public override string Description => "No need to chase the circle, the circle chases you"; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) }; - private readonly List movingObjects = new List(); + public const float SPIN_RADIUS = 50; // same as OsuAutoGeneratorBase.SPIN_RADIUS + private DrawableSpinner activeSpinner; - private double spinnerAngle; // in radians + private float spinnerAngle; // in radians public void Update(Playfield playfield) { var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; - var currentTime = playfield.Clock.CurrentTime; + double currentTime = playfield.Clock.CurrentTime; - // Avoid relocating judgment displays and hide follow points + // Judgment displays would all be cramped onto the cursor playfield.DisplayJudgements.Value = false; - (playfield as OsuPlayfield)?.ConnectionLayer.Hide(); + + // FIXME: Hide follow points + //(playfield as OsuPlayfield)?.ConnectionLayer.Hide(); // If object too old, remove from movingObjects list, otherwise move to new destination - movingObjects.RemoveAll(d => + foreach (var drawable in playfield.HitObjectContainer.AliveObjects.OfType()) { - var h = d.HitObject; - var endTime = (h as IHasEndTime)?.EndTime ?? h.StartTime; + var h = drawable.HitObject; + double endTime = h.GetEndTime(); - // Object no longer required to be moved -> remove from list - if (currentTime > endTime) - return true; - - switch (d) + switch (drawable) { case DrawableHitCircle circle: // 10ms earlier on the note to reduce chance of missing when clicking early / cursor moves fast circle.MoveTo(cursorPos, Math.Max(0, endTime - currentTime - 10)); - return false; + + break; case DrawableSlider slider: @@ -66,11 +67,12 @@ namespace osu.Game.Rulesets.Osu.Mods // Move slider so that sliderball stays on the cursor else { - slider.HeadCircle.Hide(); // temporary solution to supress HeadCircle's explosion, flash, ... at wrong location + // FIXME: Hide flashes + //slider.HeadCircle.Hide(); slider.MoveTo(cursorPos - slider.Ball.DrawPosition); } - return false; + break; case DrawableSpinner spinner: @@ -78,23 +80,32 @@ namespace osu.Game.Rulesets.Osu.Mods if (currentTime < h.StartTime) { spinner.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); - return false; } else { - spinnerAngle = 0; - activeSpinner = spinner; - return true; + // TODO: + // - get current angle to cursor + // - move clockwise(?) + // - call spinner.RotationTracker.AddRotation + + // TODO: Remove + //spinnerAngle = 0; + //activeSpinner = spinner; } - default: - return true; - } - }); + break; + default: + continue; + } + } + + // Move active spinner around the cursor if (activeSpinner != null) { - if (currentTime > (activeSpinner.HitObject as IHasEndTime)?.EndTime) + double spinnerEndTime = activeSpinner.HitObject.GetEndTime(); + + if (currentTime > spinnerEndTime) { activeSpinner = null; spinnerAngle = 0; @@ -102,29 +113,23 @@ namespace osu.Game.Rulesets.Osu.Mods else { const float additional_degrees = 4; - const int dist_from_cursor = 30; - spinnerAngle += additional_degrees * Math.PI / 180; + float added_degrees = additional_degrees * (float)Math.PI / 180; + spinnerAngle += added_degrees; + + //int spinsRequired = activeSpinner.HitObject.SpinsRequired; + //float spunDegrees = activeSpinner.Result.RateAdjustedRotation; + //double timeLeft = spinnerEndTime - currentTime; // Visual progress - activeSpinner.MoveTo(new Vector2((float)(dist_from_cursor * Math.Cos(spinnerAngle) + cursorPos.X), (float)(dist_from_cursor * Math.Sin(spinnerAngle) + cursorPos.Y))); + activeSpinner.MoveTo(new Vector2((float)(SPIN_RADIUS * Math.Cos(spinnerAngle) + cursorPos.X), (float)(SPIN_RADIUS * Math.Sin(spinnerAngle) + cursorPos.Y))); // Logical progress - activeSpinner.Disc.RotationAbsolute += additional_degrees; + activeSpinner.RotationTracker.AddRotation(added_degrees); + Console.WriteLine($"added_degrees={added_degrees}"); + //activeSpinner.Disc.RotationAbsolute += additional_degrees; } } } - - public void ApplyToDrawableHitObjects(IEnumerable drawables) - { - foreach (var drawable in drawables) - drawable.ApplyCustomUpdateState += drawableOnApplyCustomUpdateState; - } - - private void drawableOnApplyCustomUpdateState(DrawableHitObject drawable, ArmedState state) - { - if (drawable is DrawableOsuHitObject hitobject) - movingObjects.Add(hitobject); - } } /* From 27a8bfa4968672c44c64e7beb1e842894755ecf5 Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Tue, 4 Jan 2022 22:17:50 +0100 Subject: [PATCH 015/587] handle spinners and follow points --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 80 +++++-------------- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 9 ++- 2 files changed, 26 insertions(+), 63 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index 1849b48073..306e7a8b24 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -2,11 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -26,23 +24,19 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) }; - public const float SPIN_RADIUS = 50; // same as OsuAutoGeneratorBase.SPIN_RADIUS - - private DrawableSpinner activeSpinner; - private float spinnerAngle; // in radians + private const float spin_radius = 30; + private Vector2? prevCursorPos; public void Update(Playfield playfield) { var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; double currentTime = playfield.Clock.CurrentTime; - // Judgment displays would all be cramped onto the cursor + // Hide judgment displays and follow points playfield.DisplayJudgements.Value = false; + (playfield as OsuPlayfield)?.FollowPoints.Clear(); - // FIXME: Hide follow points - //(playfield as OsuPlayfield)?.ConnectionLayer.Hide(); - - // If object too old, remove from movingObjects list, otherwise move to new destination + // Move all currently alive object to new destination foreach (var drawable in playfield.HitObjectContainer.AliveObjects.OfType()) { var h = drawable.HitObject; @@ -79,64 +73,32 @@ namespace osu.Game.Rulesets.Osu.Mods // Move spinner to cursor if (currentTime < h.StartTime) { - spinner.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); + spinner.MoveTo(cursorPos + new Vector2(0, -spin_radius), Math.Max(0, h.StartTime - currentTime - 10)); } else { - // TODO: - // - get current angle to cursor - // - move clockwise(?) - // - call spinner.RotationTracker.AddRotation + // Move spinner visually + Vector2 delta = spin_radius * (spinner.Position - prevCursorPos ?? cursorPos).Normalized(); + const float angle = 3 * MathF.PI / 180; // radians per update, arbitrary value - // TODO: Remove - //spinnerAngle = 0; - //activeSpinner = spinner; + // Rotation matrix + var targetPos = new Vector2( + delta.X * MathF.Cos(angle) - delta.Y * MathF.Sin(angle) + cursorPos.X, + delta.X * MathF.Sin(angle) + delta.Y * MathF.Cos(angle) + cursorPos.Y + ); + + spinner.MoveTo(targetPos); + + // Logically finish spinner immediatly, no need for the user to click. + // Temporary workaround until spinner rotations are easier to handle, similar as Autopilot mod. + spinner.Result.RateAdjustedRotation = spinner.HitObject.SpinsRequired * 360; } break; - - default: - continue; } } - // Move active spinner around the cursor - if (activeSpinner != null) - { - double spinnerEndTime = activeSpinner.HitObject.GetEndTime(); - - if (currentTime > spinnerEndTime) - { - activeSpinner = null; - spinnerAngle = 0; - } - else - { - const float additional_degrees = 4; - float added_degrees = additional_degrees * (float)Math.PI / 180; - spinnerAngle += added_degrees; - - //int spinsRequired = activeSpinner.HitObject.SpinsRequired; - //float spunDegrees = activeSpinner.Result.RateAdjustedRotation; - //double timeLeft = spinnerEndTime - currentTime; - - // Visual progress - activeSpinner.MoveTo(new Vector2((float)(SPIN_RADIUS * Math.Cos(spinnerAngle) + cursorPos.X), (float)(SPIN_RADIUS * Math.Sin(spinnerAngle) + cursorPos.Y))); - - // Logical progress - activeSpinner.RotationTracker.AddRotation(added_degrees); - Console.WriteLine($"added_degrees={added_degrees}"); - //activeSpinner.Disc.RotationAbsolute += additional_degrees; - } - } + prevCursorPos = cursorPos; } } - - /* - * TODOs - * - fix sliders reappearing at original position after their EndTime (see https://puu.sh/E7zT4/111cf9cdc8.gif) - * - find nicer way to handle slider headcircle explosion, flash, ... - * - add Aim Assist as incompatible mod for Autoplay (?) - * - */ } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 2233a547b9..bc1e80cd12 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -31,7 +31,8 @@ namespace osu.Game.Rulesets.Osu.UI private readonly ProxyContainer approachCircles; private readonly ProxyContainer spinnerProxies; private readonly JudgementContainer judgementLayer; - private readonly FollowPointRenderer followPoints; + + public FollowPointRenderer FollowPoints { get; } public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -50,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.UI { playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both }, spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both }, - followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }, + FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }, judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both }, HitObjectContainer, judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both }, @@ -131,13 +132,13 @@ namespace osu.Game.Rulesets.Osu.UI protected override void OnHitObjectAdded(HitObject hitObject) { base.OnHitObjectAdded(hitObject); - followPoints.AddFollowPoints((OsuHitObject)hitObject); + FollowPoints.AddFollowPoints((OsuHitObject)hitObject); } protected override void OnHitObjectRemoved(HitObject hitObject) { base.OnHitObjectRemoved(hitObject); - followPoints.RemoveFollowPoints((OsuHitObject)hitObject); + FollowPoints.RemoveFollowPoints((OsuHitObject)hitObject); } private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) From 5a62760fe4f301d7f6a736f35feee14c847f5c96 Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Wed, 5 Jan 2022 13:05:22 +0100 Subject: [PATCH 016/587] hold spinners & minor adjustments --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index 306e7a8b24..cde86c8868 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -5,16 +5,16 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Osu.UI; using osuTK; using System.Linq; +using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModAimAssist : Mod, IUpdatableByPlayfield + internal class OsuModAimAssist : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset { public override string Name => "Aim Assist"; public override string Acronym => "AA"; @@ -25,7 +25,15 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) }; private const float spin_radius = 30; + private Vector2? prevCursorPos; + private OsuInputManager inputManager; + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + // Grab the input manager for future use + inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; + } public void Update(Playfield playfield) { @@ -34,20 +42,20 @@ namespace osu.Game.Rulesets.Osu.Mods // Hide judgment displays and follow points playfield.DisplayJudgements.Value = false; - (playfield as OsuPlayfield)?.FollowPoints.Clear(); + (playfield as OsuPlayfield)?.FollowPoints.Hide(); // Move all currently alive object to new destination foreach (var drawable in playfield.HitObjectContainer.AliveObjects.OfType()) { var h = drawable.HitObject; - double endTime = h.GetEndTime(); switch (drawable) { case DrawableHitCircle circle: // 10ms earlier on the note to reduce chance of missing when clicking early / cursor moves fast - circle.MoveTo(cursorPos, Math.Max(0, endTime - currentTime - 10)); + circle.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); + // FIXME: some circles cause flash at original(?) position when clicked too early break; @@ -61,16 +69,16 @@ namespace osu.Game.Rulesets.Osu.Mods // Move slider so that sliderball stays on the cursor else { - // FIXME: Hide flashes - //slider.HeadCircle.Hide(); + slider.HeadCircle.Hide(); // hide flash, triangles, ... so they don't move with slider slider.MoveTo(cursorPos - slider.Ball.DrawPosition); + // FIXME: some sliders re-appearing at their original position for a single frame when they're done } break; case DrawableSpinner spinner: - // Move spinner to cursor + // Move spinner _next_ to cursor if (currentTime < h.StartTime) { spinner.MoveTo(cursorPos + new Vector2(0, -spin_radius), Math.Max(0, h.StartTime - currentTime - 10)); @@ -89,9 +97,12 @@ namespace osu.Game.Rulesets.Osu.Mods spinner.MoveTo(targetPos); - // Logically finish spinner immediatly, no need for the user to click. - // Temporary workaround until spinner rotations are easier to handle, similar as Autopilot mod. - spinner.Result.RateAdjustedRotation = spinner.HitObject.SpinsRequired * 360; + // Move spinner logically + if (inputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false) + { + // Arbitrary value, might lead to some inconsistencies depending on clock rate, replay, ... + spinner.RotationTracker.AddRotation(2 * MathF.PI); + } } break; From 04d060aba3f95cb75899d642a964c168679524a4 Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Thu, 6 Jan 2022 10:38:30 +0100 Subject: [PATCH 017/587] update general playfield only once --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index cde86c8868..f983ece71b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -33,6 +33,10 @@ namespace osu.Game.Rulesets.Osu.Mods { // Grab the input manager for future use inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; + + // Hide judgment displays and follow points + drawableRuleset.Playfield.DisplayJudgements.Value = false; + (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); } public void Update(Playfield playfield) @@ -40,10 +44,6 @@ namespace osu.Game.Rulesets.Osu.Mods var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; double currentTime = playfield.Clock.CurrentTime; - // Hide judgment displays and follow points - playfield.DisplayJudgements.Value = false; - (playfield as OsuPlayfield)?.FollowPoints.Hide(); - // Move all currently alive object to new destination foreach (var drawable in playfield.HitObjectContainer.AliveObjects.OfType()) { From b9d2a10530695421bf5eee2ddad792718624493d Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Thu, 6 Jan 2022 14:47:58 +0100 Subject: [PATCH 018/587] adjustable assist strength + dont update spinner & running slider --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 82 ++++--------------- 1 file changed, 16 insertions(+), 66 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index f983ece71b..c7cabd7ab4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -11,6 +11,8 @@ using osu.Game.Rulesets.Osu.UI; using osuTK; using System.Linq; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Configuration; +using osu.Framework.Bindables; namespace osu.Game.Rulesets.Osu.Mods { @@ -24,16 +26,16 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) }; - private const float spin_radius = 30; - - private Vector2? prevCursorPos; - private OsuInputManager inputManager; + [SettingSource("Assist strength", "Change the distance notes should travel towards you.", 0)] + public BindableFloat AssistStrength { get; } = new BindableFloat(0.3f) + { + Precision = 0.05f, + MinValue = 0.0f, + MaxValue = 1.0f, + }; public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - // Grab the input manager for future use - inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; - // Hide judgment displays and follow points drawableRuleset.Playfield.DisplayJudgements.Value = false; (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); @@ -41,75 +43,23 @@ namespace osu.Game.Rulesets.Osu.Mods public void Update(Playfield playfield) { - var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; + Vector2 cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; double currentTime = playfield.Clock.CurrentTime; - // Move all currently alive object to new destination foreach (var drawable in playfield.HitObjectContainer.AliveObjects.OfType()) { var h = drawable.HitObject; - switch (drawable) + if (currentTime < h.StartTime && (drawable is DrawableHitCircle || drawable is DrawableSlider)) { - case DrawableHitCircle circle: + double timeMoving = currentTime - (h.StartTime - h.TimePreempt); + float percentDoneMoving = (float)(timeMoving / h.TimePreempt); + float percentDistLeft = Math.Clamp(AssistStrength.Value - percentDoneMoving + 0.1f, 0, 1); - // 10ms earlier on the note to reduce chance of missing when clicking early / cursor moves fast - circle.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); - // FIXME: some circles cause flash at original(?) position when clicked too early - - break; - - case DrawableSlider slider: - - // Move slider to cursor - if (currentTime < h.StartTime) - { - slider.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); - } - // Move slider so that sliderball stays on the cursor - else - { - slider.HeadCircle.Hide(); // hide flash, triangles, ... so they don't move with slider - slider.MoveTo(cursorPos - slider.Ball.DrawPosition); - // FIXME: some sliders re-appearing at their original position for a single frame when they're done - } - - break; - - case DrawableSpinner spinner: - - // Move spinner _next_ to cursor - if (currentTime < h.StartTime) - { - spinner.MoveTo(cursorPos + new Vector2(0, -spin_radius), Math.Max(0, h.StartTime - currentTime - 10)); - } - else - { - // Move spinner visually - Vector2 delta = spin_radius * (spinner.Position - prevCursorPos ?? cursorPos).Normalized(); - const float angle = 3 * MathF.PI / 180; // radians per update, arbitrary value - - // Rotation matrix - var targetPos = new Vector2( - delta.X * MathF.Cos(angle) - delta.Y * MathF.Sin(angle) + cursorPos.X, - delta.X * MathF.Sin(angle) + delta.Y * MathF.Cos(angle) + cursorPos.Y - ); - - spinner.MoveTo(targetPos); - - // Move spinner logically - if (inputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false) - { - // Arbitrary value, might lead to some inconsistencies depending on clock rate, replay, ... - spinner.RotationTracker.AddRotation(2 * MathF.PI); - } - } - - break; + Vector2 targetPos = drawable.Position + percentDistLeft * (cursorPos - drawable.Position); + drawable.MoveTo(targetPos, h.StartTime - currentTime); } } - - prevCursorPos = cursorPos; } } } From 197ada1a8cf43c7fcb9db2fffa072e310be22b9e Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Thu, 6 Jan 2022 16:04:38 +0100 Subject: [PATCH 019/587] naive 10hz update --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index c7cabd7ab4..89a385dbdc 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -34,6 +34,8 @@ namespace osu.Game.Rulesets.Osu.Mods MaxValue = 1.0f, }; + private DateTime? lastUpdate; + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { // Hide judgment displays and follow points @@ -43,6 +45,9 @@ namespace osu.Game.Rulesets.Osu.Mods public void Update(Playfield playfield) { + if (DateTime.Now - (lastUpdate ?? DateTime.MinValue) < TimeSpan.FromMilliseconds(100)) + return; + Vector2 cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; double currentTime = playfield.Clock.CurrentTime; @@ -60,6 +65,8 @@ namespace osu.Game.Rulesets.Osu.Mods drawable.MoveTo(targetPos, h.StartTime - currentTime); } } + + lastUpdate = DateTime.Now; } } } From b3230868cc88ca7ada83ea6c4c8f0542c333c569 Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Thu, 6 Jan 2022 16:31:30 +0100 Subject: [PATCH 020/587] use playfield clock --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index 89a385dbdc..a383b533fd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Mods MaxValue = 1.0f, }; - private DateTime? lastUpdate; + private double? lastUpdate; public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { @@ -45,11 +45,12 @@ namespace osu.Game.Rulesets.Osu.Mods public void Update(Playfield playfield) { - if (DateTime.Now - (lastUpdate ?? DateTime.MinValue) < TimeSpan.FromMilliseconds(100)) + double currentTime = playfield.Clock.CurrentTime; + + if (currentTime - (lastUpdate ?? double.MinValue) < 100) return; Vector2 cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; - double currentTime = playfield.Clock.CurrentTime; foreach (var drawable in playfield.HitObjectContainer.AliveObjects.OfType()) { @@ -66,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Mods } } - lastUpdate = DateTime.Now; + lastUpdate = currentTime; } } } From b0d61a18b05424bdc5b39b248a36c5d139ad241c Mon Sep 17 00:00:00 2001 From: pikokr Date: Fri, 7 Jan 2022 15:57:30 +0900 Subject: [PATCH 021/587] Load keyBindingContainer once on LoadComplete() & make touch area height to const --- osu.Game.Rulesets.Mania/UI/Column.cs | 43 ++++++---------------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 1e07c84d9f..cd5f3d2170 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Mania.UI { public const float COLUMN_WIDTH = 80; public const float SPECIAL_COLUMN_WIDTH = 70; + public const float TOUCH_AREA_HEIGHT = 100; /// /// The index of this column as part of the whole playfield. @@ -44,60 +45,34 @@ namespace osu.Game.Rulesets.Mania.UI private readonly GameplaySampleTriggerSource sampleTriggerSource; - public class ColumnTouchInputArea : Container + public class ColumnTouchInputArea : Drawable { - private Column column => (Column)Parent; - - private Container hintContainer; - public ColumnTouchInputArea() { RelativeSizeAxes = Axes.X; Anchor = Anchor.BottomCentre; Origin = Anchor.BottomCentre; - Height = 100; - InternalChild = hintContainer = new Container - { - RelativeSizeAxes = Axes.Both, - BorderColour = Color4.Red, - BorderThickness = 5, - Masking = true, - }; + Height = TOUCH_AREA_HEIGHT; } + private Column column => (Column)Parent; + protected override void LoadComplete() { - hintContainer.Delay(1000).FadeOutFromOne(500, Easing.OutSine); + keyBindingContainer = (ManiaInputManager.RulesetKeyBindingContainer)((ManiaInputManager)GetContainingInputManager()).KeyBindingContainer; } private ManiaInputManager.RulesetKeyBindingContainer keyBindingContainer { get; set; } - private ManiaInputManager.RulesetKeyBindingContainer getKeyBindingContainer() - { - return keyBindingContainer ??= (ManiaInputManager.RulesetKeyBindingContainer)((ManiaInputManager)GetContainingInputManager()).KeyBindingContainer; - } - protected override bool OnTouchDown(TouchDownEvent e) { - getKeyBindingContainer().TriggerPressed(column.Action.Value); - return base.OnTouchDown(e); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - getKeyBindingContainer().TriggerPressed(column.Action.Value); - - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - getKeyBindingContainer().TriggerReleased(column.Action.Value); + keyBindingContainer.TriggerPressed(column.Action.Value); + return false; } protected override void OnTouchUp(TouchUpEvent e) { - getKeyBindingContainer().TriggerReleased(column.Action.Value); + keyBindingContainer.TriggerReleased(column.Action.Value); } } From 2a59735525abb6d481fadc044808f497ace7f2a4 Mon Sep 17 00:00:00 2001 From: mk-56 Date: Sat, 15 Jan 2022 21:43:28 +0100 Subject: [PATCH 022/587] Initial commit --- .../Mods/CatchModFlashlight.cs | 21 +++------- .../Mods/ManiaModFlashlight.cs | 13 ++++-- .../Mods/OsuModFlashlight.cs | 42 ++++++++++--------- .../Mods/TaikoModFlashlight.cs | 20 ++++----- osu.Game/Rulesets/Mods/ModFlashlight.cs | 42 +++++++++++++++++++ 5 files changed, 89 insertions(+), 49 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index f399f48ebd..e5da168dc6 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -15,9 +15,10 @@ namespace osu.Game.Rulesets.Catch.Mods { public override double ScoreMultiplier => 1.12; - private const float default_flashlight_size = 350; + public override bool DefaultComboDependency => true; + public override float DefaultRadius => 350; - public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield); + public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value); private CatchPlayfield playfield; @@ -31,10 +32,10 @@ namespace osu.Game.Rulesets.Catch.Mods { private readonly CatchPlayfield playfield; - public CatchFlashlight(CatchPlayfield playfield) + public CatchFlashlight(CatchPlayfield playfield, bool isRadiusBasedOnCombo, float initialRadius) : base(isRadiusBasedOnCombo, initialRadius) { this.playfield = playfield; - FlashlightSize = new Vector2(0, getSizeFor(0)); + FlashlightSize = new Vector2(0, GetRadiusFor(0)); } protected override void Update() @@ -44,19 +45,9 @@ namespace osu.Game.Rulesets.Catch.Mods FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this); } - private float getSizeFor(int combo) - { - if (combo > 200) - return default_flashlight_size * 0.8f; - else if (combo > 100) - return default_flashlight_size * 0.9f; - else - return default_flashlight_size; - } - protected override void OnComboChange(ValueChangedEvent e) { - this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); + this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetRadiusFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); } protected override string FragmentShader => "CircularFlashlight"; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index 86a00271e9..676b5f3842 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -16,17 +16,19 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModHidden) }; - private const float default_flashlight_size = 180; + public override bool DefaultComboDependency => false; + public override float DefaultRadius => 180; - public override Flashlight CreateFlashlight() => new ManiaFlashlight(); + public override Flashlight CreateFlashlight() => new ManiaFlashlight(ChangeRadius.Value, InitialRadius.Value); private class ManiaFlashlight : Flashlight { private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); - public ManiaFlashlight() + public ManiaFlashlight(bool isRadiusBasedOnCombo, float initialRadius) + : base(isRadiusBasedOnCombo, initialRadius) { - FlashlightSize = new Vector2(0, default_flashlight_size); + FlashlightSize = new Vector2(DrawWidth, GetRadiusFor(0)); AddLayout(flashlightProperties); } @@ -46,9 +48,12 @@ namespace osu.Game.Rulesets.Mania.Mods protected override void OnComboChange(ValueChangedEvent e) { + this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, GetRadiusFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); } protected override string FragmentShader => "RectangularFlashlight"; } } } + + diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index 300a9d48aa..f15527460c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.UI; using osuTK; namespace osu.Game.Rulesets.Osu.Mods @@ -21,13 +20,16 @@ namespace osu.Game.Rulesets.Osu.Mods { public override double ScoreMultiplier => 1.12; - private const float default_flashlight_size = 180; + public override bool DefaultComboDependency => true; + + //private const float default_flashlight_size = 180; + public override float DefaultRadius => 180; private const double default_follow_delay = 120; private OsuFlashlight flashlight; - public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(); + public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(ChangeRadius.Value, InitialRadius.Value, FollowDelay.Value); public void ApplyToDrawableHitObject(DrawableHitObject drawable) { @@ -35,13 +37,6 @@ namespace osu.Game.Rulesets.Osu.Mods s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange; } - public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) - { - base.ApplyToDrawableRuleset(drawableRuleset); - - flashlight.FollowDelay = FollowDelay.Value; - } - [SettingSource("Follow delay", "Milliseconds until the flashlight reaches the cursor")] public BindableNumber FollowDelay { get; } = new BindableDouble(default_follow_delay) { @@ -54,9 +49,15 @@ namespace osu.Game.Rulesets.Osu.Mods { public double FollowDelay { private get; set; } - public OsuFlashlight() + //public float InitialRadius { private get; set; } + public bool ChangeRadius { private get; set; } + + public OsuFlashlight(bool isRadiusBasedOnCombo, float initialRadius, double followDelay) + : base(isRadiusBasedOnCombo, initialRadius) { - FlashlightSize = new Vector2(0, getSizeFor(0)); + FollowDelay = followDelay; + + FlashlightSize = new Vector2(0, GetRadiusFor(0)); } public void OnSliderTrackingChange(ValueChangedEvent e) @@ -78,17 +79,20 @@ namespace osu.Game.Rulesets.Osu.Mods private float getSizeFor(int combo) { - if (combo > 200) - return default_flashlight_size * 0.8f; - else if (combo > 100) - return default_flashlight_size * 0.9f; - else - return default_flashlight_size; + if (ChangeRadius) + { + if (combo > 200) + return InitialRadius * 0.8f; + else if (combo > 100) + return InitialRadius * 0.9f; + } + + return InitialRadius; } protected override void OnComboChange(ValueChangedEvent e) { - this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); + this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetRadiusFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); } protected override string FragmentShader => "CircularFlashlight"; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index 0a325f174e..29f29863c0 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -16,9 +16,12 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override double ScoreMultiplier => 1.12; - private const float default_flashlight_size = 250; + public override bool DefaultComboDependency => true; - public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield); + //private const float default_flashlight_size = 250; + public override float DefaultRadius => 250; + + public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value); private TaikoPlayfield playfield; @@ -33,7 +36,8 @@ namespace osu.Game.Rulesets.Taiko.Mods private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); private readonly TaikoPlayfield taikoPlayfield; - public TaikoFlashlight(TaikoPlayfield taikoPlayfield) + public TaikoFlashlight(TaikoPlayfield taikoPlayfield, bool isRadiusBasedOnCombo, float initialRadius) + : base(isRadiusBasedOnCombo, initialRadius) { this.taikoPlayfield = taikoPlayfield; FlashlightSize = getSizeFor(0); @@ -43,15 +47,9 @@ namespace osu.Game.Rulesets.Taiko.Mods private Vector2 getSizeFor(int combo) { - float size = default_flashlight_size; - - if (combo > 200) - size *= 0.8f; - else if (combo > 100) - size *= 0.9f; - // Preserve flashlight size through the playfield's aspect adjustment. - return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); + // return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); + return new Vector2(0, GetRadiusFor(combo) * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); } protected override void OnComboChange(ValueChangedEvent e) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index a77a83b36c..bff0e2f12d 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.Timing; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.OpenGL.Vertices; using osu.Game.Rulesets.Objects; @@ -32,8 +33,26 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyIncrease; public override string Description => "Restricted view area."; + [SettingSource("Change radius based on combo", "Decrease the flashlight radius as combo increases.")] + public Bindable ChangeRadius { get; private set; } + + [SettingSource("Initial radius", "Initial radius of the flashlight area.")] + public BindableNumber InitialRadius { get; private set; } + + public abstract float DefaultRadius { get; } + + public abstract bool DefaultComboDependency { get; } + internal ModFlashlight() { + InitialRadius = new BindableFloat + { + MinValue = 90f, + MaxValue = 250f, + Precision = 5f, + }; + + ChangeRadius = new BindableBool(DefaultComboDependency); } } @@ -93,6 +112,16 @@ namespace osu.Game.Rulesets.Mods public List Breaks; + public readonly bool IsRadiusBasedOnCombo; + + public readonly float InitialRadius; + + protected Flashlight(bool isRadiusBasedOnCombo, float initialRadius) + { + IsRadiusBasedOnCombo = isRadiusBasedOnCombo; + InitialRadius = initialRadius; + } + [BackgroundDependencyLoader] private void load(ShaderManager shaderManager) { @@ -124,6 +153,19 @@ namespace osu.Game.Rulesets.Mods protected abstract string FragmentShader { get; } + protected float GetRadiusFor(int combo) + { + if (IsRadiusBasedOnCombo) + { + if (combo > 200) + return InitialRadius * 0.8f; + else if (combo > 100) + return InitialRadius * 0.9f; + } + + return InitialRadius; + } + private Vector2 flashlightPosition; protected Vector2 FlashlightPosition From 57cc2f78933fb89c3226c7ab28bc1a3fe6c0e8da Mon Sep 17 00:00:00 2001 From: mk-56 Date: Sun, 16 Jan 2022 14:26:26 +0100 Subject: [PATCH 023/587] Adjustment to size values of FL per mode --- osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs | 3 ++- osu.Game/Rulesets/Mods/ModFlashlight.cs | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index e5da168dc6..9d9fa5aed4 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -32,7 +32,8 @@ namespace osu.Game.Rulesets.Catch.Mods { private readonly CatchPlayfield playfield; - public CatchFlashlight(CatchPlayfield playfield, bool isRadiusBasedOnCombo, float initialRadius) : base(isRadiusBasedOnCombo, initialRadius) + public CatchFlashlight(CatchPlayfield playfield, bool isRadiusBasedOnCombo, float initialRadius) + : base(isRadiusBasedOnCombo, initialRadius) { this.playfield = playfield; FlashlightSize = new Vector2(0, GetRadiusFor(0)); diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index bff0e2f12d..c218ab45fe 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -45,10 +45,10 @@ namespace osu.Game.Rulesets.Mods internal ModFlashlight() { - InitialRadius = new BindableFloat + InitialRadius = new BindableFloat(DefaultRadius) { - MinValue = 90f, - MaxValue = 250f, + MinValue = DefaultRadius * .5f, + MaxValue = DefaultRadius * 1.5f, Precision = 5f, }; From 511a607599215e8981321e16df99c4a32918f67c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 18:28:17 +0800 Subject: [PATCH 024/587] Display performance breakdown in a tooltip --- .../Difficulty/ManiaPerformanceAttributes.cs | 11 +++ .../Difficulty/OsuPerformanceAttributes.cs | 13 +++ .../Difficulty/TaikoPerformanceAttributes.cs | 10 +++ .../Difficulty/PerformanceAttributes.cs | 10 +++ .../Difficulty/PerformanceDisplayAttribute.cs | 26 ++++++ osu.Game/Scoring/ScorePerformanceCache.cs | 9 ++- .../Statistics/PerformanceStatistic.cs | 30 ++++--- .../Statistics/PerformanceStatisticTooltip.cs | 80 +++++++++++++++++++ 8 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs index da9634ba47..0d3a53f3f3 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; @@ -16,5 +17,15 @@ namespace osu.Game.Rulesets.Mania.Difficulty [JsonProperty("scaled_score")] public double ScaledScore { get; set; } + + public override IEnumerable GetAttributesForDisplay() + { + foreach (var attribute in base.GetAttributesForDisplay()) + yield return attribute; + + yield return new PerformanceDisplayAttribute("Difficulty", Difficulty); + yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); + yield return new PerformanceDisplayAttribute("Scaled Score", ScaledScore); + } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 6c7760d144..db7ca6af88 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; @@ -22,5 +23,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + + public override IEnumerable GetAttributesForDisplay() + { + foreach (var attribute in base.GetAttributesForDisplay()) + yield return attribute; + + yield return new PerformanceDisplayAttribute("Aim", Aim); + yield return new PerformanceDisplayAttribute("Speed", Speed); + yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); + yield return new PerformanceDisplayAttribute("Flashlight Bonus", Flashlight); + yield return new PerformanceDisplayAttribute("Effective Miss Count", EffectiveMissCount); + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index 80552880ea..efbcdd3703 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; @@ -13,5 +14,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("accuracy")] public double Accuracy { get; set; } + + public override IEnumerable GetAttributesForDisplay() + { + foreach (var attribute in base.GetAttributesForDisplay()) + yield return attribute; + + yield return new PerformanceDisplayAttribute("Difficulty", Difficulty); + yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); + } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index 025b38257c..98c6c75f6c 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; namespace osu.Game.Rulesets.Difficulty @@ -12,5 +13,14 @@ namespace osu.Game.Rulesets.Difficulty /// [JsonProperty("pp")] public double Total { get; set; } + + /// + /// Return a for each attribute so that a performance breakdown can be displayed. + /// + /// + public virtual IEnumerable GetAttributesForDisplay() + { + yield return new PerformanceDisplayAttribute("Total", Total); + } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs new file mode 100644 index 0000000000..5bb505847e --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. +namespace osu.Game.Rulesets.Difficulty +{ + /// + /// Data for displaying a performance attribute to user. Includes a display name for clarity. + /// + public class PerformanceDisplayAttribute + { + /// + /// A custom display name for the attribute. + /// + public string DisplayName { get; } + + /// + /// The associated attribute value. + /// + public double Value { get; } + + public PerformanceDisplayAttribute(string displayName, double value) + { + DisplayName = displayName; + Value = value; + } + } +} diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index b855343505..a428a66aae 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Rulesets.Difficulty; namespace osu.Game.Scoring { @@ -15,7 +16,7 @@ namespace osu.Game.Scoring /// A component which performs and acts as a central cache for performance calculations of locally databased scores. /// Currently not persisted between game sessions. /// - public class ScorePerformanceCache : MemoryCachingComponent + public class ScorePerformanceCache : MemoryCachingComponent { [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } @@ -27,10 +28,10 @@ namespace osu.Game.Scoring /// /// The score to do the calculation on. /// An optional to cancel the operation. - public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) => + public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) => GetAsync(new PerformanceCacheLookup(score), token); - protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default) + protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default) { var score = lookup.ScoreInfo; @@ -44,7 +45,7 @@ namespace osu.Game.Scoring var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Value.Attributes, score); - return calculator?.Calculate().Total; + return calculator?.Calculate(); } public readonly struct PerformanceCacheLookup diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index d6e4cfbe51..888552e568 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -7,12 +7,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Difficulty; using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatistic : StatisticDisplay + public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip { private readonly ScoreInfo score; @@ -22,6 +24,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private RollingCounter counter; + private PerformanceAttributes attributes; + public PerformanceStatistic(ScoreInfo score) : base("PP") { @@ -31,21 +35,17 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics [BackgroundDependencyLoader] private void load(ScorePerformanceCache performanceCache) { - if (score.PP.HasValue) - { - setPerformanceValue(score.PP.Value); - } - else - { - performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())), cancellationTokenSource.Token); - } + performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())), cancellationTokenSource.Token); } - private void setPerformanceValue(double? pp) + private void setPerformanceValue(PerformanceAttributes pp) { - if (pp.HasValue) - performance.Value = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); + if (pp != null) + { + attributes = pp; + performance.Value = (int)Math.Round(pp.Total, MidpointRounding.AwayFromZero); + } } public override void Appear() @@ -65,5 +65,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }; + + public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); + + public PerformanceAttributes TooltipContent => attributes; } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs new file mode 100644 index 0000000000..32ba231e42 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Difficulty; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip + { + private readonly Box background; + + protected override Container Content { get; } + + public PerformanceStatisticTooltip() + { + AutoSizeAxes = Axes.Both; + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + Content = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 } + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.Gray3; + } + + protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); + + private PerformanceAttributes lastAttributes; + + public void SetContent(PerformanceAttributes attributes) + { + if (attributes == lastAttributes) + return; + + lastAttributes = attributes; + + UpdateDisplay(attributes); + } + + protected virtual void UpdateDisplay(PerformanceAttributes attributes) + { + Content.Clear(); + + foreach (PerformanceDisplayAttribute attr in attributes.GetAttributesForDisplay()) + { + Content.Add(new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = $"{attr.DisplayName}: {(int)Math.Round(attr.Value, MidpointRounding.AwayFromZero)}" + }); + } + } + + public void Move(Vector2 pos) => Position = pos; + } +} From 85c60bfc2d50b1c5b27348c7a8e821fbeca3b340 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 20:36:18 +0800 Subject: [PATCH 025/587] Improve tooltip design --- .../Statistics/PerformanceStatisticTooltip.cs | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 32ba231e42..618a94a309 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.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.Globalization; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -16,6 +17,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip { private readonly Box background; + private Colour4 textColor; protected override Container Content { get; } @@ -44,6 +46,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private void load(OsuColour colours) { background.Colour = colours.Gray3; + textColor = colours.BlueLighter; } protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); @@ -67,10 +70,35 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics foreach (PerformanceDisplayAttribute attr in attributes.GetAttributesForDisplay()) { - Content.Add(new OsuSpriteText + Content.Add(new GridContainer { - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = $"{attr.DisplayName}: {(int)Math.Round(attr.Value, MidpointRounding.AwayFromZero)}" + AutoSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 140), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = attr.DisplayName, + Colour = textColor + }, + new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Text = ((int)Math.Round(attr.Value, MidpointRounding.AwayFromZero)).ToString(CultureInfo.CurrentCulture) + } + } + } }); } } From d014fef179a9248afc4f4b09237fa84fb16beeaf Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 20:36:36 +0800 Subject: [PATCH 026/587] Hide confusing attributes --- osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs | 1 - osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs | 1 - osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs index 0d3a53f3f3..48895cd389 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs @@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty yield return new PerformanceDisplayAttribute("Difficulty", Difficulty); yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); - yield return new PerformanceDisplayAttribute("Scaled Score", ScaledScore); } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index db7ca6af88..0a685b7cd6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -33,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return new PerformanceDisplayAttribute("Speed", Speed); yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); yield return new PerformanceDisplayAttribute("Flashlight Bonus", Flashlight); - yield return new PerformanceDisplayAttribute("Effective Miss Count", EffectiveMissCount); } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index 98c6c75f6c..ee1868ecff 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Difficulty /// /// Return a for each attribute so that a performance breakdown can be displayed. + /// Some attributes may be omitted if they are not meant for display. /// /// public virtual IEnumerable GetAttributesForDisplay() From b81fc675e89b6df6161e18a45f6ae392fdbd183a Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 20:45:25 +0800 Subject: [PATCH 027/587] Include PropertyName in PerformanceDisplayAttribute --- .../Difficulty/ManiaPerformanceAttributes.cs | 4 ++-- .../Difficulty/OsuPerformanceAttributes.cs | 8 ++++---- .../Difficulty/TaikoPerformanceAttributes.cs | 4 ++-- osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs | 2 +- .../Rulesets/Difficulty/PerformanceDisplayAttribute.cs | 8 +++++++- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs index 48895cd389..17c864a268 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs @@ -23,8 +23,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty foreach (var attribute in base.GetAttributesForDisplay()) yield return attribute; - yield return new PerformanceDisplayAttribute("Difficulty", Difficulty); - yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); + yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty); + yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy); } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 0a685b7cd6..0aeaf7669f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -29,10 +29,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty foreach (var attribute in base.GetAttributesForDisplay()) yield return attribute; - yield return new PerformanceDisplayAttribute("Aim", Aim); - yield return new PerformanceDisplayAttribute("Speed", Speed); - yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); - yield return new PerformanceDisplayAttribute("Flashlight Bonus", Flashlight); + yield return new PerformanceDisplayAttribute(nameof(Aim), "Aim", Aim); + yield return new PerformanceDisplayAttribute(nameof(Speed), "Speed", Speed); + yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy); + yield return new PerformanceDisplayAttribute(nameof(Flashlight), "Flashlight Bonus", Flashlight); } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index efbcdd3703..fa5c0202dd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty foreach (var attribute in base.GetAttributesForDisplay()) yield return attribute; - yield return new PerformanceDisplayAttribute("Difficulty", Difficulty); - yield return new PerformanceDisplayAttribute("Accuracy", Accuracy); + yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty); + yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy); } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index ee1868ecff..9bdb5f8f6f 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Difficulty /// public virtual IEnumerable GetAttributesForDisplay() { - yield return new PerformanceDisplayAttribute("Total", Total); + yield return new PerformanceDisplayAttribute(nameof(Total), "Total", Total); } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs index 5bb505847e..e95cb03053 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs @@ -7,6 +7,11 @@ namespace osu.Game.Rulesets.Difficulty /// public class PerformanceDisplayAttribute { + /// + /// Name of the attribute property in . + /// + public string PropertyName { get; } + /// /// A custom display name for the attribute. /// @@ -17,8 +22,9 @@ namespace osu.Game.Rulesets.Difficulty /// public double Value { get; } - public PerformanceDisplayAttribute(string displayName, double value) + public PerformanceDisplayAttribute(string propertyName, string displayName, double value) { + PropertyName = propertyName; DisplayName = displayName; Value = value; } From c49cd60487584fd8c2d6e208bc9158942e6ce519 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 21:26:55 +0800 Subject: [PATCH 028/587] Add bar chart to tooltip --- .../Statistics/PerformanceStatisticTooltip.cs | 108 ++++++++++++------ 1 file changed, 74 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 618a94a309..ab7156e0c5 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -2,22 +2,27 @@ // See the LICENCE file in the repository root for full licence text. using System; using System.Globalization; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Difficulty; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Statistics { public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip { private readonly Box background; - private Colour4 textColor; + private Colour4 totalColour; + private Colour4 textColour; protected override Container Content { get; } @@ -46,10 +51,16 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private void load(OsuColour colours) { background.Colour = colours.Gray3; - textColor = colours.BlueLighter; + totalColour = colours.Blue; + textColour = colours.BlueLighter; + } + + protected override void PopIn() + { + if (lastAttributes.GetAttributesForDisplay().Count() > 1) + this.FadeIn(200, Easing.OutQuint); } - protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); private PerformanceAttributes lastAttributes; @@ -64,42 +75,71 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics UpdateDisplay(attributes); } + private Drawable createAttributeItem(PerformanceDisplayAttribute attribute, double attributeSum) + { + bool isTotal = attribute.PropertyName == nameof(PerformanceAttributes.Total); + return new GridContainer + { + AutoSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 110), + new Dimension(GridSizeMode.Absolute, 140), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = attribute.DisplayName, + Colour = isTotal ? totalColour : textColour + }, + new Bar + { + Alpha = isTotal ? 0 : 1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Width = 130, + Height = 5, + BackgroundColour = Color4.White.Opacity(0.5f), + Length = (float)(attribute.Value / attributeSum), + Margin = new MarginPadding { Left = 5, Right = 5 } + }, + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Text = ((int)Math.Round(attribute.Value, MidpointRounding.AwayFromZero)).ToString(CultureInfo.CurrentCulture), + Colour = isTotal ? totalColour : textColour + } + } + } + }; + } + protected virtual void UpdateDisplay(PerformanceAttributes attributes) { Content.Clear(); - foreach (PerformanceDisplayAttribute attr in attributes.GetAttributesForDisplay()) + var displayAttributes = attributes.GetAttributesForDisplay(); + + double attributeSum = displayAttributes + .Where(attr => attr.PropertyName != nameof(PerformanceAttributes.Total)) + .Sum(attr => attr.Value); + + foreach (PerformanceDisplayAttribute attr in displayAttributes) { - Content.Add(new GridContainer - { - AutoSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 140), - new Dimension(GridSizeMode.AutoSize) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = attr.DisplayName, - Colour = textColor - }, - new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = ((int)Math.Round(attr.Value, MidpointRounding.AwayFromZero)).ToString(CultureInfo.CurrentCulture) - } - } - } - }); + Content.Add(createAttributeItem(attr, attributeSum)); } } From eddf45329445f8b43cb37b65cbeaa3d3bce0f332 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 21:32:13 +0800 Subject: [PATCH 029/587] Fix code quality issues --- osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs | 1 + .../Ranking/Expanded/Statistics/PerformanceStatistic.cs | 6 ++---- .../Expanded/Statistics/PerformanceStatisticTooltip.cs | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs index e95cb03053..7958bc174e 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs @@ -1,5 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. + namespace osu.Game.Rulesets.Difficulty { /// diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 888552e568..4fd6964a68 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -24,8 +24,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private RollingCounter counter; - private PerformanceAttributes attributes; - public PerformanceStatistic(ScoreInfo score) : base("PP") { @@ -43,7 +41,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { if (pp != null) { - attributes = pp; + TooltipContent = pp; performance.Value = (int)Math.Round(pp.Total, MidpointRounding.AwayFromZero); } } @@ -68,6 +66,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); - public PerformanceAttributes TooltipContent => attributes; + public PerformanceAttributes TooltipContent { get; private set; } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index ab7156e0c5..e0bbf91381 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -1,5 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. + using System; using System.Globalization; using System.Linq; From 83387cb00bcb89b9974b8bcc223b56f78cb72f3e Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 17 Jan 2022 21:41:17 +0800 Subject: [PATCH 030/587] Add a comment --- .../Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index e0bbf91381..deef30124c 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -58,6 +58,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics protected override void PopIn() { + // Don't display the tooltip if "Total" is the only item if (lastAttributes.GetAttributesForDisplay().Count() > 1) this.FadeIn(200, Easing.OutQuint); } From bd308ca38c5afbddbc18cebaa4e8effa1d4cca90 Mon Sep 17 00:00:00 2001 From: mk-56 Date: Mon, 17 Jan 2022 15:15:25 +0100 Subject: [PATCH 031/587] Cleanup --- .../Mods/ManiaModFlashlight.cs | 2 -- osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs | 15 ++------------- .../Mods/TaikoModFlashlight.cs | 1 - 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index 676b5f3842..4fff736c57 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -55,5 +55,3 @@ namespace osu.Game.Rulesets.Mania.Mods } } } - - diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index f15527460c..f381d14ffe 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -27,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.Mods private const double default_follow_delay = 120; + + private OsuFlashlight flashlight; public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(ChangeRadius.Value, InitialRadius.Value, FollowDelay.Value); @@ -77,19 +79,6 @@ namespace osu.Game.Rulesets.Osu.Mods return base.OnMouseMove(e); } - private float getSizeFor(int combo) - { - if (ChangeRadius) - { - if (combo > 200) - return InitialRadius * 0.8f; - else if (combo > 100) - return InitialRadius * 0.9f; - } - - return InitialRadius; - } - protected override void OnComboChange(ValueChangedEvent e) { this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetRadiusFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index 29f29863c0..76f7c45b75 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Taiko.Mods private Vector2 getSizeFor(int combo) { // Preserve flashlight size through the playfield's aspect adjustment. - // return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); return new Vector2(0, GetRadiusFor(combo) * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); } From 43e5bd731cbfa0fff4d2a0825e10ceaefb56d599 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 18 Jan 2022 21:57:12 +0800 Subject: [PATCH 032/587] Compare performance to a perfect play --- .../Statistics/PerformanceStatistic.cs | 97 +++++++++++++++++-- .../Statistics/PerformanceStatisticTooltip.cs | 38 ++++---- 2 files changed, 107 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 4fd6964a68..493f6a2dc9 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -2,19 +2,24 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; +using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip + public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip { private readonly ScoreInfo score; @@ -24,6 +29,15 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private RollingCounter counter; + [Resolved] + private ScorePerformanceCache performanceCache { get; set; } + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + public PerformanceStatistic(ScoreInfo score) : base("PP") { @@ -31,18 +45,74 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics } [BackgroundDependencyLoader] - private void load(ScorePerformanceCache performanceCache) + private void load() { - performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())), cancellationTokenSource.Token); + Task.WhenAll( + // actual performance + performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token), + // performance for a perfect play + getPerfectPerformance(score) + ).ContinueWith(attr => + { + PerformanceAttributes[] result = attr.GetResultSafely(); + setPerformanceValue(new PerformanceBreakdown { Performance = result[0], PerfectPerformance = result[1] }); + }); } - private void setPerformanceValue(PerformanceAttributes pp) + private async Task getPerfectPerformance(ScoreInfo originalScore) { - if (pp != null) + ScoreInfo perfectScore = await getPerfectScore(originalScore).ConfigureAwait(false); + return await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationTokenSource.Token).ConfigureAwait(false); + } + + private Task getPerfectScore(ScoreInfo originalScore) + { + return Task.Factory.StartNew(() => { - TooltipContent = pp; - performance.Value = (int)Math.Round(pp.Total, MidpointRounding.AwayFromZero); + var beatmap = beatmapManager.GetWorkingBeatmap(originalScore.BeatmapInfo).GetPlayableBeatmap(originalScore.Ruleset, originalScore.Mods); + ScoreInfo perfectPlay = originalScore.DeepClone(); + perfectPlay.Accuracy = 1; + perfectPlay.Passed = true; + + // create statistics assuming all hit objects have perfect hit result + var statistics = beatmap.HitObjects + .Select(ho => ho.CreateJudgement().MaxResult) + .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) + .ToDictionary(pair => pair.hitResult, pair => pair.count); + perfectPlay.Statistics = statistics; + + // calculate max combo + var difficulty = difficultyCache.GetDifficultyAsync( + beatmap.BeatmapInfo, + originalScore.Ruleset, + originalScore.Mods, + cancellationTokenSource.Token + ).GetResultSafely(); + perfectPlay.MaxCombo = difficulty?.MaxCombo ?? originalScore.MaxCombo; + + // calculate total score + ScoreProcessor scoreProcessor = originalScore.Ruleset.CreateInstance().CreateScoreProcessor(); + perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); + + // compute rank achieved + // default to SS, then adjust the rank with mods + perfectPlay.Rank = ScoreRank.X; + + foreach (IApplicableToScoreProcessor mod in perfectPlay.Mods.OfType()) + { + perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1); + } + + return perfectPlay; + }, cancellationTokenSource.Token); + } + + private void setPerformanceValue(PerformanceBreakdown breakdown) + { + if (breakdown != null) + { + TooltipContent = breakdown; + performance.Value = (int)Math.Round(breakdown.Performance.Total, MidpointRounding.AwayFromZero); } } @@ -64,8 +134,15 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Origin = Anchor.TopCentre }; - public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); + public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); - public PerformanceAttributes TooltipContent { get; private set; } + public PerformanceBreakdown TooltipContent { get; private set; } + + public class PerformanceBreakdown + { + public PerformanceAttributes Performance { get; set; } + + public PerformanceAttributes PerfectPerformance { get; set; } + } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index deef30124c..564c195a3d 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -1,11 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Globalization; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -19,7 +18,9 @@ using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip + using static PerformanceStatistic; + + public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip { private readonly Box background; private Colour4 totalColour; @@ -59,27 +60,30 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics protected override void PopIn() { // Don't display the tooltip if "Total" is the only item - if (lastAttributes.GetAttributesForDisplay().Count() > 1) + if (currentPerformance.Performance.GetAttributesForDisplay().Count() > 1) this.FadeIn(200, Easing.OutQuint); } protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); - private PerformanceAttributes lastAttributes; + private PerformanceBreakdown currentPerformance; - public void SetContent(PerformanceAttributes attributes) + public void SetContent(PerformanceBreakdown performance) { - if (attributes == lastAttributes) + if (performance == currentPerformance) return; - lastAttributes = attributes; + currentPerformance = performance; - UpdateDisplay(attributes); + UpdateDisplay(performance); } - private Drawable createAttributeItem(PerformanceDisplayAttribute attribute, double attributeSum) + private Drawable createAttributeItem(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) { bool isTotal = attribute.PropertyName == nameof(PerformanceAttributes.Total); + float fraction = (float)(attribute.Value / perfectAttribute.Value); + if (float.IsNaN(fraction)) + fraction = 0; return new GridContainer { AutoSizeAxes = Axes.Both, @@ -113,7 +117,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Width = 130, Height = 5, BackgroundColour = Color4.White.Opacity(0.5f), - Length = (float)(attribute.Value / attributeSum), + Length = fraction, Margin = new MarginPadding { Left = 5, Right = 5 } }, new OsuSpriteText @@ -121,7 +125,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = ((int)Math.Round(attribute.Value, MidpointRounding.AwayFromZero)).ToString(CultureInfo.CurrentCulture), + Text = fraction.ToLocalisableString("0%"), Colour = isTotal ? totalColour : textColour } } @@ -129,19 +133,17 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics }; } - protected virtual void UpdateDisplay(PerformanceAttributes attributes) + protected virtual void UpdateDisplay(PerformanceBreakdown performance) { Content.Clear(); - var displayAttributes = attributes.GetAttributesForDisplay(); + var displayAttributes = performance.Performance.GetAttributesForDisplay(); - double attributeSum = displayAttributes - .Where(attr => attr.PropertyName != nameof(PerformanceAttributes.Total)) - .Sum(attr => attr.Value); + var perfectDisplayAttributes = performance.PerfectPerformance.GetAttributesForDisplay(); foreach (PerformanceDisplayAttribute attr in displayAttributes) { - Content.Add(createAttributeItem(attr, attributeSum)); + Content.Add(createAttributeItem(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName))); } } From a5b53c01c813a94fadadd5f3102990f0aece2137 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 18 Jan 2022 21:59:51 +0800 Subject: [PATCH 033/587] Add comments and tidy up --- .../Ranking/Expanded/Statistics/PerformanceStatistic.cs | 6 ++++++ .../Expanded/Statistics/PerformanceStatisticTooltip.cs | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 493f6a2dc9..f00eb9d71f 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -140,8 +140,14 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public class PerformanceBreakdown { + /// + /// Actual gameplay performance. + /// public PerformanceAttributes Performance { get; set; } + /// + /// Performance of a perfect play for comparison. + /// public PerformanceAttributes PerfectPerformance { get; set; } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 564c195a3d..7209db53f6 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip { private readonly Box background; - private Colour4 totalColour; + private Colour4 titleColor; private Colour4 textColour; protected override Container Content { get; } @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private void load(OsuColour colours) { background.Colour = colours.Gray3; - totalColour = colours.Blue; + titleColor = colours.Blue; textColour = colours.BlueLighter; } @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.Regular), Text = attribute.DisplayName, - Colour = isTotal ? totalColour : textColour + Colour = isTotal ? titleColor : textColour }, new Bar { @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.SemiBold), Text = fraction.ToLocalisableString("0%"), - Colour = isTotal ? totalColour : textColour + Colour = isTotal ? titleColor : textColour } } } From 31e03e99cd08f1bfd3c648461a6c17e959b0495a Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 18 Jan 2022 22:11:43 +0800 Subject: [PATCH 034/587] Improve display of "total PP" --- osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs | 2 +- .../Expanded/Statistics/PerformanceStatisticTooltip.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index 9bdb5f8f6f..f210c669a6 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Difficulty /// public virtual IEnumerable GetAttributesForDisplay() { - yield return new PerformanceDisplayAttribute(nameof(Total), "Total", Total); + yield return new PerformanceDisplayAttribute(nameof(Total), "Final PP", Total); } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 7209db53f6..528ccafd41 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -84,6 +85,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics float fraction = (float)(attribute.Value / perfectAttribute.Value); if (float.IsNaN(fraction)) fraction = 0; + string text = fraction.ToLocalisableString("0%").ToString(); + + if (isTotal) + text = (int)Math.Round(attribute.Value, MidpointRounding.AwayFromZero) + "/" + (int)Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero); + return new GridContainer { AutoSizeAxes = Axes.Both, @@ -111,12 +117,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics }, new Bar { - Alpha = isTotal ? 0 : 1, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Width = 130, Height = 5, BackgroundColour = Color4.White.Opacity(0.5f), + Colour = isTotal ? titleColor : textColour, Length = fraction, Margin = new MarginPadding { Left = 5, Right = 5 } }, @@ -125,7 +131,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = fraction.ToLocalisableString("0%"), + Text = text, Colour = isTotal ? titleColor : textColour } } From 0d708efb734571e8c2617b830b07c8406cdf5e31 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 19 Jan 2022 15:33:33 +0800 Subject: [PATCH 035/587] Split off `PerformanceBreakdown` and its own calculation logic --- .../Difficulty/PerformanceBreakdown.cs | 21 +++++ .../PerformanceBreakdownCalculator.cs | 90 +++++++++++++++++++ .../Statistics/PerformanceStatistic.cs | 80 +---------------- .../Statistics/PerformanceStatisticTooltip.cs | 2 - 4 files changed, 115 insertions(+), 78 deletions(-) create mode 100644 osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs create mode 100644 osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs new file mode 100644 index 0000000000..273d8613c5 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.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. + +namespace osu.Game.Rulesets.Difficulty +{ + /// + /// Data for generating a performance breakdown by comparing performance to a perfect play. + /// + public class PerformanceBreakdown + { + /// + /// Actual gameplay performance. + /// + public PerformanceAttributes Performance { get; set; } + + /// + /// Performance of a perfect play for comparison. + /// + public PerformanceAttributes PerfectPerformance { get; set; } + } +} diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs new file mode 100644 index 0000000000..fad7937a99 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.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. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Extensions; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Rulesets.Difficulty +{ + public class PerformanceBreakdownCalculator + { + private readonly BeatmapManager beatmapManager; + private readonly BeatmapDifficultyCache difficultyCache; + private readonly ScorePerformanceCache performanceCache; + + public PerformanceBreakdownCalculator(BeatmapManager beatmapManager, BeatmapDifficultyCache difficultyCache, ScorePerformanceCache performanceCache) + { + this.beatmapManager = beatmapManager; + this.difficultyCache = difficultyCache; + this.performanceCache = performanceCache; + } + + [ItemCanBeNull] + public async Task CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default) + { + PerformanceAttributes performance = await performanceCache.CalculatePerformanceAsync(score, cancellationToken).ConfigureAwait(false); + + ScoreInfo perfectScore = await getPerfectScore(score, cancellationToken).ConfigureAwait(false); + if (perfectScore == null) + return null; + + PerformanceAttributes perfectPerformance = await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationToken).ConfigureAwait(false); + + return new PerformanceBreakdown { Performance = performance, PerfectPerformance = perfectPerformance }; + } + + [ItemCanBeNull] + private Task getPerfectScore(ScoreInfo score, CancellationToken cancellationToken = default) + { + return Task.Factory.StartNew(() => + { + IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(score.BeatmapInfo).GetPlayableBeatmap(score.Ruleset, score.Mods); + ScoreInfo perfectPlay = score.DeepClone(); + perfectPlay.Accuracy = 1; + perfectPlay.Passed = true; + + // calculate max combo + var difficulty = difficultyCache.GetDifficultyAsync( + beatmap.BeatmapInfo, + score.Ruleset, + score.Mods, + cancellationToken + ).GetResultSafely(); + + if (difficulty == null) + return null; + + perfectPlay.MaxCombo = difficulty.Value.MaxCombo; + + // create statistics assuming all hit objects have perfect hit result + var statistics = beatmap.HitObjects + .Select(ho => ho.CreateJudgement().MaxResult) + .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) + .ToDictionary(pair => pair.hitResult, pair => pair.count); + perfectPlay.Statistics = statistics; + + // calculate total score + ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor(); + perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); + + // compute rank achieved + // default to SS, then adjust the rank with mods + perfectPlay.Rank = ScoreRank.X; + + foreach (IApplicableToScoreProcessor mod in perfectPlay.Mods.OfType()) + { + perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1); + } + + return perfectPlay; + }, cancellationToken); + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index f00eb9d71f..f934dd1836 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -13,13 +11,11 @@ using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip + public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip { private readonly ScoreInfo score; @@ -47,64 +43,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics [BackgroundDependencyLoader] private void load() { - Task.WhenAll( - // actual performance - performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token), - // performance for a perfect play - getPerfectPerformance(score) - ).ContinueWith(attr => - { - PerformanceAttributes[] result = attr.GetResultSafely(); - setPerformanceValue(new PerformanceBreakdown { Performance = result[0], PerfectPerformance = result[1] }); - }); - } - - private async Task getPerfectPerformance(ScoreInfo originalScore) - { - ScoreInfo perfectScore = await getPerfectScore(originalScore).ConfigureAwait(false); - return await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationTokenSource.Token).ConfigureAwait(false); - } - - private Task getPerfectScore(ScoreInfo originalScore) - { - return Task.Factory.StartNew(() => - { - var beatmap = beatmapManager.GetWorkingBeatmap(originalScore.BeatmapInfo).GetPlayableBeatmap(originalScore.Ruleset, originalScore.Mods); - ScoreInfo perfectPlay = originalScore.DeepClone(); - perfectPlay.Accuracy = 1; - perfectPlay.Passed = true; - - // create statistics assuming all hit objects have perfect hit result - var statistics = beatmap.HitObjects - .Select(ho => ho.CreateJudgement().MaxResult) - .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) - .ToDictionary(pair => pair.hitResult, pair => pair.count); - perfectPlay.Statistics = statistics; - - // calculate max combo - var difficulty = difficultyCache.GetDifficultyAsync( - beatmap.BeatmapInfo, - originalScore.Ruleset, - originalScore.Mods, - cancellationTokenSource.Token - ).GetResultSafely(); - perfectPlay.MaxCombo = difficulty?.MaxCombo ?? originalScore.MaxCombo; - - // calculate total score - ScoreProcessor scoreProcessor = originalScore.Ruleset.CreateInstance().CreateScoreProcessor(); - perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); - - // compute rank achieved - // default to SS, then adjust the rank with mods - perfectPlay.Rank = ScoreRank.X; - - foreach (IApplicableToScoreProcessor mod in perfectPlay.Mods.OfType()) - { - perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1); - } - - return perfectPlay; - }, cancellationTokenSource.Token); + new PerformanceBreakdownCalculator(beatmapManager, difficultyCache, performanceCache) + .CalculateAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => setPerformanceValue(t.GetResultSafely())); } private void setPerformanceValue(PerformanceBreakdown breakdown) @@ -137,18 +78,5 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); public PerformanceBreakdown TooltipContent { get; private set; } - - public class PerformanceBreakdown - { - /// - /// Actual gameplay performance. - /// - public PerformanceAttributes Performance { get; set; } - - /// - /// Performance of a perfect play for comparison. - /// - public PerformanceAttributes PerfectPerformance { get; set; } - } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 528ccafd41..930bfba96a 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -19,8 +19,6 @@ using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - using static PerformanceStatistic; - public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip { private readonly Box background; From dd42c89260c26d1cd719242c78388217f23a2f6d Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 19 Jan 2022 16:08:45 +0800 Subject: [PATCH 036/587] Feed more info to the temporary score processor for more accurate total score --- osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index fad7937a99..17cb1de303 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -72,6 +72,8 @@ namespace osu.Game.Rulesets.Difficulty // calculate total score ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor(); + scoreProcessor.HighestCombo.Value = perfectPlay.MaxCombo; + scoreProcessor.Mods.Value = perfectPlay.Mods; perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); // compute rank achieved From 261fae68735ef6df0b43c9d1599869972ba18a1c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 20 Jan 2022 08:39:33 +0800 Subject: [PATCH 037/587] Move checks out of PopIn() --- .../Ranking/Expanded/Statistics/PerformanceStatistic.cs | 4 +++- .../Expanded/Statistics/PerformanceStatisticTooltip.cs | 7 +------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index f934dd1836..bda147cf22 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -50,7 +51,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private void setPerformanceValue(PerformanceBreakdown breakdown) { - if (breakdown != null) + // Don't display the tooltip if "Total" is the only item + if (breakdown != null && breakdown.Performance.GetAttributesForDisplay().Count() > 1) { TooltipContent = breakdown; performance.Value = (int)Math.Round(breakdown.Performance.Total, MidpointRounding.AwayFromZero); diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 930bfba96a..44e5c366bb 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -56,12 +56,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics textColour = colours.BlueLighter; } - protected override void PopIn() - { - // Don't display the tooltip if "Total" is the only item - if (currentPerformance.Performance.GetAttributesForDisplay().Count() > 1) - this.FadeIn(200, Easing.OutQuint); - } + protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); From 42d904acee23490e51086cbae21fd9bf3aff9045 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 20 Jan 2022 12:50:28 +0800 Subject: [PATCH 038/587] Remove blocking calls and add back Task.WhenAll --- .../PerformanceBreakdownCalculator.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 17cb1de303..3c9c00ff3b 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -29,21 +28,30 @@ namespace osu.Game.Rulesets.Difficulty [ItemCanBeNull] public async Task CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default) { - PerformanceAttributes performance = await performanceCache.CalculatePerformanceAsync(score, cancellationToken).ConfigureAwait(false); + PerformanceAttributes[] performanceArray = await Task.WhenAll( + // compute actual performance + performanceCache.CalculatePerformanceAsync(score, cancellationToken), + // compute performance for perfect play + getPerfectPerformance(score, cancellationToken) + ).ConfigureAwait(false); + return new PerformanceBreakdown { Performance = performanceArray[0], PerfectPerformance = performanceArray[1] }; + } + + [ItemCanBeNull] + private async Task getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default) + { ScoreInfo perfectScore = await getPerfectScore(score, cancellationToken).ConfigureAwait(false); if (perfectScore == null) return null; - PerformanceAttributes perfectPerformance = await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationToken).ConfigureAwait(false); - - return new PerformanceBreakdown { Performance = performance, PerfectPerformance = perfectPerformance }; + return await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationToken).ConfigureAwait(false); } [ItemCanBeNull] private Task getPerfectScore(ScoreInfo score, CancellationToken cancellationToken = default) { - return Task.Factory.StartNew(() => + return Task.Run(async () => { IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(score.BeatmapInfo).GetPlayableBeatmap(score.Ruleset, score.Mods); ScoreInfo perfectPlay = score.DeepClone(); @@ -51,12 +59,12 @@ namespace osu.Game.Rulesets.Difficulty perfectPlay.Passed = true; // calculate max combo - var difficulty = difficultyCache.GetDifficultyAsync( + var difficulty = await difficultyCache.GetDifficultyAsync( beatmap.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken - ).GetResultSafely(); + ).ConfigureAwait(false); if (difficulty == null) return null; From 6c97fbd3f28f8374822e41950ca293a3d5a1e62f Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 20 Jan 2022 13:06:00 +0800 Subject: [PATCH 039/587] Calculate perfect performance without caching --- .../PerformanceBreakdownCalculator.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 3c9c00ff3b..5cf63d0102 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -39,20 +39,11 @@ namespace osu.Game.Rulesets.Difficulty } [ItemCanBeNull] - private async Task getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default) - { - ScoreInfo perfectScore = await getPerfectScore(score, cancellationToken).ConfigureAwait(false); - if (perfectScore == null) - return null; - - return await performanceCache.CalculatePerformanceAsync(perfectScore, cancellationToken).ConfigureAwait(false); - } - - [ItemCanBeNull] - private Task getPerfectScore(ScoreInfo score, CancellationToken cancellationToken = default) + private Task getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default) { return Task.Run(async () => { + Ruleset ruleset = score.Ruleset.CreateInstance(); IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(score.BeatmapInfo).GetPlayableBeatmap(score.Ruleset, score.Mods); ScoreInfo perfectPlay = score.DeepClone(); perfectPlay.Accuracy = 1; @@ -79,7 +70,7 @@ namespace osu.Game.Rulesets.Difficulty perfectPlay.Statistics = statistics; // calculate total score - ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor(); + ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.HighestCombo.Value = perfectPlay.MaxCombo; scoreProcessor.Mods.Value = perfectPlay.Mods; perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); @@ -93,7 +84,9 @@ namespace osu.Game.Rulesets.Difficulty perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1); } - return perfectPlay; + // calculate performance for this perfect score + // ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes + return ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate(); }, cancellationToken); } } From e0fe8af365535a20c077cc397a69b2bf801c9dba Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 21 Jan 2022 08:54:08 +0800 Subject: [PATCH 040/587] Schedule setPerformanceValue --- .../Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index bda147cf22..158fd82b29 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { new PerformanceBreakdownCalculator(beatmapManager, difficultyCache, performanceCache) .CalculateAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => setPerformanceValue(t.GetResultSafely())); + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()))); } private void setPerformanceValue(PerformanceBreakdown breakdown) From 2006620a2c02d60c83792803b4276cb163a3b4a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 17:55:27 +0900 Subject: [PATCH 041/587] Fix `IntroScreen` retrieving and iterating all realm beatmap sets --- osu.Game/Screens/Menu/IntroScreen.cs | 82 ++++++++++++++-------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index d98cb8056f..db9d9b83c4 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osuTK; using osuTK.Graphics; +using Realms; namespace osu.Game.Screens.Menu { @@ -93,58 +94,57 @@ namespace osu.Game.Screens.Menu MenuMusic = config.GetBindable(OsuSetting.MenuMusic); seeya = audio.Samples.Get(SeeyaSampleName); - ILive setInfo = null; - // if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection. if (!MenuMusic.Value) { - var sets = beatmaps.GetAllUsableBeatmapSets(); - - if (sets.Count > 0) + realmContextFactory.Run(realm => { - setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID); - setInfo?.PerformRead(s => + var sets = realm.All().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection(); + + int setCount = sets.Count; + + if (sets.Any()) + { + var found = sets[RNG.Next(0, setCount - 1)].Beatmaps.FirstOrDefault(); + + if (found != null) + initialBeatmap = beatmaps.GetWorkingBeatmap(found); + } + }); + + // we generally want a song to be playing on startup, so use the intro music even if a user has specified not to if no other track is available. + if (initialBeatmap == null) + { + if (!loadThemedIntro()) + { + // 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)).GetResultSafely(); + + import?.PerformWrite(b => b.Protected = true); + + loadThemedIntro(); + } + } + + bool loadThemedIntro() + { + var setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash); + + if (setInfo == null) + return false; + + setInfo.PerformRead(s => { if (s.Beatmaps.Count == 0) return; - initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0]); + initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps.First()); }); + + return UsingThemedIntro = initialBeatmap != null; } } - - // we generally want a song to be playing on startup, so use the intro music even if a user has specified not to if no other track is available. - if (setInfo == null) - { - if (!loadThemedIntro()) - { - // 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)).GetResultSafely(); - - import?.PerformWrite(b => b.Protected = true); - - loadThemedIntro(); - } - } - - bool loadThemedIntro() - { - setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash); - - if (setInfo == null) - return false; - - setInfo.PerformRead(s => - { - if (s.Beatmaps.Count == 0) - return; - - initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps.First()); - }); - - return UsingThemedIntro = initialBeatmap != null; - } } public override void OnResuming(IScreen last) From 18bf690a30bc9d5c5c6d53133469fc462bcf62ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 18:13:21 +0900 Subject: [PATCH 042/587] Add `Register` method for subscription purposes and update safeties --- osu.Game/Database/RealmContextFactory.cs | 28 ++++++++++++++++++++++ osu.Game/Database/RealmObjectExtensions.cs | 6 ++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 888ffb1dd5..169333bff8 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -88,6 +88,10 @@ namespace osu.Game.Database } } + internal static bool CurrentThreadSubscriptionsAllowed => current_thread_subscriptions_allowed.Value; + + private static readonly ThreadLocal current_thread_subscriptions_allowed = new ThreadLocal(); + /// /// Construct a new instance of a realm context factory. /// @@ -222,6 +226,30 @@ namespace osu.Game.Database } } + /// + /// Run work on realm that will be run every time the update thread realm context gets recycled. + /// + /// The work to run. + public void Register(Action action) + { + if (!ThreadSafety.IsUpdateThread && context != null) + throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); + + if (ThreadSafety.IsUpdateThread) + { + current_thread_subscriptions_allowed.Value = true; + action(Context); + current_thread_subscriptions_allowed.Value = false; + } + else + { + current_thread_subscriptions_allowed.Value = true; + using (var realm = createContext()) + action(realm); + current_thread_subscriptions_allowed.Value = false; + } + } + private Realm createContext() { if (isDisposed) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index c25aeab336..c30e1699b9 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Runtime.Serialization; using AutoMapper; using AutoMapper.Internal; -using osu.Framework.Development; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Models; @@ -272,9 +271,8 @@ namespace osu.Game.Database public static IDisposable? QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) where T : RealmObjectBase { - // Subscriptions can only work on the main thread. - if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException("Cannot subscribe for realm notifications from a non-update thread."); + if (!RealmContextFactory.CurrentThreadSubscriptionsAllowed) + throw new InvalidOperationException($"Make sure to call {nameof(RealmContextFactory)}.{nameof(RealmContextFactory.Register)}"); return collection.SubscribeForNotifications(callback); } From 45aea9add5209953d7e76a093a74f5ad91732a60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 18:50:25 +0900 Subject: [PATCH 043/587] Implement full subscription flow --- osu.Game/Database/RealmContextFactory.cs | 46 +++++++++++++++++------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 169333bff8..62717eb880 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -80,6 +82,10 @@ namespace osu.Game.Database { context = createContext(); Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}"); + + // Resubscribe any subscriptions + foreach (var action in subscriptionActions.Keys) + registerSubscription(action); } // creating a context will ensure our schema is up-to-date and migrated. @@ -226,26 +232,42 @@ namespace osu.Game.Database } } + private readonly Dictionary, IDisposable?> subscriptionActions = new Dictionary, IDisposable?>(); + /// /// Run work on realm that will be run every time the update thread realm context gets recycled. /// - /// The work to run. - public void Register(Action action) + /// The work to run. Return value should be an from QueryAsyncWithNotifications, or an to clean up any bindings. + /// An which should be disposed to unsubscribe any inner subscription. + public IDisposable Register(Func action) { - if (!ThreadSafety.IsUpdateThread && context != null) - throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException(@$"{nameof(Register)} must be called from the update thread."); - if (ThreadSafety.IsUpdateThread) + subscriptionActions.Add(action, null); + registerSubscription(action); + + return new InvokeOnDisposal(() => { - current_thread_subscriptions_allowed.Value = true; - action(Context); - current_thread_subscriptions_allowed.Value = false; - } - else + // TODO: this likely needs to be run on the update thread. + if (subscriptionActions.TryGetValue(action, out var unsubscriptionAction)) + { + unsubscriptionAction?.Dispose(); + subscriptionActions.Remove(action); + } + }); + } + + private void registerSubscription(Func action) + { + Debug.Assert(ThreadSafety.IsUpdateThread); + + lock (contextLock) { + Debug.Assert(context != null); + current_thread_subscriptions_allowed.Value = true; - using (var realm = createContext()) - action(realm); + subscriptionActions[action] = action(context); current_thread_subscriptions_allowed.Value = false; } } From 1f157d729dbc0d1bc2cdbed374b41bbef650e121 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 19:39:49 +0900 Subject: [PATCH 044/587] Update existing subscriptions to new style Fix missing detach calls in `MusicController` --- osu.Game.Tests/Database/GeneralUsageTests.cs | 3 ++- osu.Game.Tests/Database/RealmLiveTests.cs | 4 ++- osu.Game/Database/RealmContextFactory.cs | 5 +--- .../Bindings/DatabasedKeyBindingContainer.cs | 26 +++++++++--------- osu.Game/Online/BeatmapDownloadTracker.cs | 4 +-- .../OnlinePlayBeatmapAvailabilityTracker.cs | 4 +-- osu.Game/Online/ScoreDownloadTracker.cs | 4 +-- osu.Game/Overlays/MusicController.cs | 17 +++++++----- .../Sections/DebugSettings/MemorySettings.cs | 4 +++ .../Overlays/Settings/Sections/SkinSection.cs | 22 +++++++-------- osu.Game/Screens/Select/BeatmapCarousel.cs | 17 ++++++------ .../Screens/Select/Carousel/TopLocalRank.cs | 25 ++++++++--------- .../Select/Leaderboards/BeatmapLeaderboard.cs | 27 ++++++++++--------- osu.Game/Screens/Spectate/SpectatorScreen.cs | 20 +++++++------- 14 files changed, 98 insertions(+), 84 deletions(-) diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 9ebe94b383..c82c1b6e59 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Database { bool callbackRan = false; - realmFactory.Run(realm => + realmFactory.Register(realm => { var subscription = realm.All().QueryAsyncWithNotifications((sender, changes, error) => { @@ -60,6 +60,7 @@ namespace osu.Game.Tests.Database realmFactory.Run(r => r.Refresh()); subscription?.Dispose(); + return null; }); Assert.IsTrue(callbackRan); diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 2f16df4624..5549c140f6 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -234,7 +234,7 @@ namespace osu.Game.Tests.Database { int changesTriggered = 0; - realmFactory.Run(outerRealm => + realmFactory.Register(outerRealm => { outerRealm.All().QueryAsyncWithNotifications(gotChange); ILive? liveBeatmap = null; @@ -277,6 +277,8 @@ namespace osu.Game.Tests.Database r.Remove(resolved); }); }); + + return null; }); void gotChange(IRealmCollection sender, ChangeSet changes, Exception error) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 62717eb880..c11ff61039 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -244,7 +244,6 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException(@$"{nameof(Register)} must be called from the update thread."); - subscriptionActions.Add(action, null); registerSubscription(action); return new InvokeOnDisposal(() => @@ -264,10 +263,8 @@ namespace osu.Game.Database lock (contextLock) { - Debug.Assert(context != null); - current_thread_subscriptions_allowed.Value = true; - subscriptionActions[action] = action(context); + subscriptionActions[action] = action(Context); current_thread_subscriptions_allowed.Value = false; } } diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 03b069d431..5a9797ffce 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -23,7 +23,6 @@ namespace osu.Game.Input.Bindings private readonly int? variant; private IDisposable realmSubscription; - private IQueryable realmKeyBindings; [Resolved] private RealmContextFactory realmFactory { get; set; } @@ -47,22 +46,25 @@ namespace osu.Game.Input.Bindings throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided."); } + private IQueryable realmKeyBindings + { + get + { + string rulesetName = ruleset?.ShortName; + return realmFactory.Context.All() + .Where(b => b.RulesetName == rulesetName && b.Variant == variant); + } + } + protected override void LoadComplete() { - string rulesetName = ruleset?.ShortName; - - realmKeyBindings = realmFactory.Context.All() - .Where(b => b.RulesetName == rulesetName && b.Variant == variant); - - realmSubscription = realmKeyBindings + realmSubscription = realmFactory.Register(realm => realmKeyBindings .QueryAsyncWithNotifications((sender, changes, error) => { - // first subscription ignored as we are handling this in LoadComplete. - if (changes == null) - return; - + // The first fire of this is a bit redundant as this is being called in base.LoadComplete, + // but this is safest in case the subscription is restored after a context recycle. ReloadMappings(); - }); + })); base.LoadComplete(); } diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index be5bdea6f1..d50ab5a347 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realmContextFactory.Context.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realmContextFactory.Register(realm => realm.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); @@ -54,7 +54,7 @@ namespace osu.Game.Online attachDownload(Downloader.GetExistingDownload(beatmapSetInfo)); }); } - }); + })); } private void downloadBegan(ArchiveDownloadRequest request) => Schedule(() => diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 1f77b1d383..fdcf2b39c6 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -78,13 +78,13 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = filteredBeatmaps().QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realmContextFactory.Register(realm => filteredBeatmaps().QueryAsyncWithNotifications((items, changes, ___) => { if (changes == null) return; Scheduler.AddOnce(updateAvailability); - }); + })); }, true); } diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index b34586567d..de1d6fd94a 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; - realmSubscription = realmContextFactory.Context.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realmContextFactory.Register(realm => realm.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); @@ -59,7 +59,7 @@ namespace osu.Game.Online attachDownload(Downloader.GetExistingDownload(scoreInfo)); }); } - }); + })); } private void downloadBegan(ArchiveDownloadRequest request) => Schedule(() => diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 70f8332295..01a2c9d354 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -80,26 +80,31 @@ namespace osu.Game.Overlays mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } + private IQueryable availableBeatmaps => realmFactory.Context + .All() + .Where(s => !s.DeletePending); + protected override void LoadComplete() { base.LoadComplete(); - var availableBeatmaps = realmFactory.Context - .All() - .Where(s => !s.DeletePending); - // ensure we're ready before completing async load. // probably not a good way of handling this (as there is a period we aren't watching for changes until the realm subscription finishes up. foreach (var s in availableBeatmaps) - beatmapSets.Add(s); + beatmapSets.Add(s.Detach()); - beatmapSubscription = availableBeatmaps.QueryAsyncWithNotifications(beatmapsChanged); + beatmapSubscription = realmFactory.Register(realm => availableBeatmaps.QueryAsyncWithNotifications(beatmapsChanged)); } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) { if (changes == null) + { + beatmapSets.Clear(); + foreach (var s in sender) + beatmapSets.Add(s.Detach()); return; + } foreach (int i in changes.InsertedIndices) beatmapSets.Insert(i, sender[i].Detach()); diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index 8d4fc5fc9f..84a54b208c 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -33,6 +33,10 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings using (realmFactory.BlockAllOperations()) { } + + // retrieve context to revive realm subscriptions. + // TODO: should we do this from OsuGame or RealmContextFactory or something? Answer: yes. + var _ = realmFactory.Context; } }, }; diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 0fa6d78d4b..11a7275168 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -50,7 +50,12 @@ namespace osu.Game.Overlays.Settings.Sections private RealmContextFactory realmFactory { get; set; } private IDisposable realmSubscription; - private IQueryable realmSkins; + + private IQueryable realmSkins => + realmFactory.Context.All() + .Where(s => !s.DeletePending) + .OrderByDescending(s => s.Protected) // protected skins should be at the top. + .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase); [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor) @@ -78,20 +83,13 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Current = dropdownBindable; - realmSkins = realmFactory.Context.All() - .Where(s => !s.DeletePending) - .OrderByDescending(s => s.Protected) // protected skins should be at the top. - .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase); - - realmSubscription = realmSkins + realmSubscription = realmFactory.Register(realm => realmSkins .QueryAsyncWithNotifications((sender, changes, error) => { - if (changes == null) - return; - - // Eventually this should be handling the individual changes rather than refreshing the whole dropdown. + // The first fire of this is a bit redundant due to the call below, + // but this is safest in case the subscription is restored after a context recycle. updateItems(); - }); + })); updateItems(); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f8cee2704b..6f3f467170 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -190,13 +190,13 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); - subscriptionSets = getBeatmapSets(realmFactory.Context).QueryAsyncWithNotifications(beatmapSetsChanged); - subscriptionBeatmaps = realmFactory.Context.All().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged); + subscriptionSets = realmFactory.Register(realm => getBeatmapSets(realm).QueryAsyncWithNotifications(beatmapSetsChanged)); + subscriptionBeatmaps = realmFactory.Register(realm => realm.All().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged)); // Can't use main subscriptions because we can't lookup deleted indices. // https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595. - subscriptionDeletedSets = realmFactory.Context.All().Where(s => s.DeletePending && !s.Protected).QueryAsyncWithNotifications(deletedBeatmapSetsChanged); - subscriptionHiddenBeatmaps = realmFactory.Context.All().Where(b => b.Hidden).QueryAsyncWithNotifications(beatmapsChanged); + subscriptionDeletedSets = realmFactory.Register(realm => realm.All().Where(s => s.DeletePending && !s.Protected).QueryAsyncWithNotifications(deletedBeatmapSetsChanged)); + subscriptionHiddenBeatmaps = realmFactory.Register(realm => realm.All().Where(b => b.Hidden).QueryAsyncWithNotifications(beatmapsChanged)); } private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) @@ -552,10 +552,11 @@ namespace osu.Game.Screens.Select private void signalBeatmapsLoaded() { - Debug.Assert(BeatmapSetsLoaded == false); - - BeatmapSetsChanged?.Invoke(); - BeatmapSetsLoaded = true; + if (!BeatmapSetsLoaded) + { + BeatmapSetsChanged?.Invoke(); + BeatmapSetsLoaded = true; + } itemsCache.Invalidate(); } diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 7ac99f4935..ee3930364b 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -48,18 +48,19 @@ namespace osu.Game.Screens.Select.Carousel ruleset.BindValueChanged(_ => { scoreSubscription?.Dispose(); - scoreSubscription = realmFactory.Context.All() - .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" - + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" - + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName) - .OrderByDescending(s => s.TotalScore) - .QueryAsyncWithNotifications((items, changes, ___) => - { - Rank = items.FirstOrDefault()?.Rank; - // Required since presence is changed via IsPresent override - Invalidate(Invalidation.Presence); - }); + scoreSubscription = realmFactory.Register(realm => + realm.All() + .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" + + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" + + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName) + .OrderByDescending(s => s.TotalScore) + .QueryAsyncWithNotifications((items, changes, ___) => + { + Rank = items.FirstOrDefault()?.Rank; + // Required since presence is changed via IsPresent override + Invalidate(Invalidation.Presence); + })); }, true); } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index da52b43ab6..954c2a6413 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -44,9 +44,13 @@ namespace osu.Game.Screens.Select.Leaderboards beatmapInfo = value; Scores = null; - UpdateScores(); - if (IsLoaded) - refreshRealmSubscription(); + if (IsOnlineScope) + UpdateScores(); + else + { + if (IsLoaded) + refreshRealmSubscription(); + } } } @@ -109,15 +113,14 @@ namespace osu.Game.Screens.Select.Leaderboards if (beatmapInfo == null) return; - scoreSubscription = realmFactory.Context.All() - .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID) - .QueryAsyncWithNotifications((_, changes, ___) => - { - if (changes == null) - return; - - RefreshScores(); - }); + scoreSubscription = realmFactory.Register(realm => + realm.All() + .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID) + .QueryAsyncWithNotifications((_, changes, ___) => + { + if (!IsOnlineScope) + RefreshScores(); + })); } protected override void Reset() diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index dd586bdd37..9fa5bb8562 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -79,17 +79,17 @@ namespace osu.Game.Screens.Spectate playingUserStates.BindTo(spectatorClient.PlayingUserStates); playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); - realmSubscription = realmContextFactory.Context - .All() - .Where(s => !s.DeletePending) - .QueryAsyncWithNotifications((items, changes, ___) => - { - if (changes?.InsertedIndices == null) - return; + realmSubscription = realmContextFactory.Register(realm => + realm.All() + .Where(s => !s.DeletePending) + .QueryAsyncWithNotifications((items, changes, ___) => + { + if (changes?.InsertedIndices == null) + return; - foreach (int c in changes.InsertedIndices) - beatmapUpdated(items[c]); - }); + foreach (int c in changes.InsertedIndices) + beatmapUpdated(items[c]); + })); foreach ((int id, var _) in userMap) spectatorClient.WatchUser(id); From 63226f7def9f10d10460e43be72a139852f83cc8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 19:56:32 +0900 Subject: [PATCH 045/587] Remove pointless initial `MusicController` beatmap set population Looks to pass tests and all usages look safe enough. --- osu.Game/Overlays/MusicController.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 01a2c9d354..c19a069343 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -87,12 +87,6 @@ namespace osu.Game.Overlays protected override void LoadComplete() { base.LoadComplete(); - - // ensure we're ready before completing async load. - // probably not a good way of handling this (as there is a period we aren't watching for changes until the realm subscription finishes up. - foreach (var s in availableBeatmaps) - beatmapSets.Add(s.Detach()); - beatmapSubscription = realmFactory.Register(realm => availableBeatmaps.QueryAsyncWithNotifications(beatmapsChanged)); } From a86c0014fe3093069a23f7014c537d31f6fdf8bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 20:07:43 +0900 Subject: [PATCH 046/587] Remove unnecessary exception/check --- osu.Game/Overlays/MusicController.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index c19a069343..0671e6d2c7 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -30,16 +30,7 @@ namespace osu.Game.Overlays [Resolved] private BeatmapManager beatmaps { get; set; } - public IBindableList BeatmapSets - { - get - { - if (LoadState < LoadState.Ready) - throw new InvalidOperationException($"{nameof(BeatmapSets)} should not be accessed before the music controller is loaded."); - - return beatmapSets; - } - } + public IBindableList BeatmapSets => beatmapSets; /// /// Point in time after which the current track will be restarted on triggering a "previous track" action. From 3b11235d3c65c7416e8e092d0a8d4aeb45c0c85c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 20:45:10 +0900 Subject: [PATCH 047/587] Fix potentially cyclic subscription registration if a subscription's delegate accesses `Context` --- osu.Game/Database/RealmContextFactory.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c11ff61039..d9c66ccc69 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -88,6 +88,8 @@ namespace osu.Game.Database registerSubscription(action); } + Debug.Assert(context != null); + // creating a context will ensure our schema is up-to-date and migrated. return context; } @@ -261,10 +263,13 @@ namespace osu.Game.Database { Debug.Assert(ThreadSafety.IsUpdateThread); + // Get context outside of flag update to ensure beyond doubt this can't be cyclic. + var realm = Context; + lock (contextLock) { current_thread_subscriptions_allowed.Value = true; - subscriptionActions[action] = action(Context); + subscriptionActions[action] = action(realm); current_thread_subscriptions_allowed.Value = false; } } From 9b63f15e68577a2d2b88d2613a6f61aff554097a Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Fri, 21 Jan 2022 13:58:30 +0100 Subject: [PATCH 048/587] Add failing test --- .../Visual/Menus/TestSceneLoginPanel.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs index 4754a73f83..642cc68de5 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs @@ -8,6 +8,8 @@ using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays.Login; +using osu.Game.Users.Drawables; +using osuTK.Input; namespace osu.Game.Tests.Visual.Menus { @@ -15,6 +17,7 @@ namespace osu.Game.Tests.Visual.Menus public class TestSceneLoginPanel : OsuManualInputManagerTestScene { private LoginPanel loginPanel; + private int hideCount; [SetUpSteps] public void SetUpSteps() @@ -26,6 +29,7 @@ namespace osu.Game.Tests.Visual.Menus Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, + RequestHide = () => hideCount++, }); }); } @@ -51,5 +55,22 @@ namespace osu.Game.Tests.Visual.Menus AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); } + + [Test] + public void TestClickingOnFlagClosesPanel() + { + AddStep("reset hide count", () => hideCount = 0); + + AddStep("logout", () => API.Logout()); + AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + + AddStep("click on flag", () => + { + InputManager.MoveMouseTo(loginPanel.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("hide requested", () => hideCount == 1); + } } } From 529610ee2e9c0d73c3a9c72c0531d4b716264644 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Fri, 21 Jan 2022 14:01:49 +0100 Subject: [PATCH 049/587] Call the UserPanel `Action` when clicking on the flag --- osu.Game/Users/Drawables/UpdateableFlag.cs | 8 ++++++++ osu.Game/Users/ExtendedUserPanel.cs | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs index 7db834bf83..e5debc0683 100644 --- a/osu.Game/Users/Drawables/UpdateableFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableFlag.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -23,6 +24,12 @@ namespace osu.Game.Users.Drawables /// public bool ShowPlaceholderOnNull = true; + /// + /// Perform an action in addition to showing the country ranking. + /// This should be used to perform auxiliary tasks and not as a primary action for clicking a flag (to maintain a consistent UX). + /// + public Action Action; + public UpdateableFlag(Country country = null) { Country = country; @@ -52,6 +59,7 @@ namespace osu.Game.Users.Drawables protected override bool OnClick(ClickEvent e) { + Action?.Invoke(); rankingsOverlay?.ShowCountry(Country); return true; } diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index fc5e1eca5f..d0f693c37c 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -53,7 +53,8 @@ namespace osu.Game.Users protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country) { - Size = new Vector2(39, 26) + Size = new Vector2(39, 26), + Action = Action, }; protected SpriteIcon CreateStatusIcon() => statusIcon = new SpriteIcon From ad3a01dc06215e17458357189cac15b9c603d093 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 22:40:18 +0900 Subject: [PATCH 050/587] Use a more reliable method of reviving the update thread realm after blocking --- osu.Game/Database/RealmContextFactory.cs | 44 +++++++++++-------- .../Sections/DebugSettings/MemorySettings.cs | 4 -- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index d9c66ccc69..6fb9a2996b 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -69,30 +69,29 @@ namespace osu.Game.Database private Realm? context; - public Realm Context + public Realm Context => ensureUpdateContext(); + + private Realm ensureUpdateContext() { - get + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException(@$"Use {nameof(createContext)} when performing realm operations from a non-update thread"); + + lock (contextLock) { - if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException(@$"Use {nameof(createContext)} when performing realm operations from a non-update thread"); - - lock (contextLock) + if (context == null) { - if (context == null) - { - context = createContext(); - Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}"); + context = createContext(); + Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}"); - // Resubscribe any subscriptions - foreach (var action in subscriptionActions.Keys) - registerSubscription(action); - } - - Debug.Assert(context != null); - - // creating a context will ensure our schema is up-to-date and migrated. - return context; + // Resubscribe any subscriptions + foreach (var action in subscriptionActions.Keys) + registerSubscription(action); } + + Debug.Assert(context != null); + + // creating a context will ensure our schema is up-to-date and migrated. + return context; } } @@ -506,6 +505,8 @@ namespace osu.Game.Database if (isDisposed) throw new ObjectDisposedException(nameof(RealmContextFactory)); + SynchronizationContext syncContext; + try { contextCreationLock.Wait(); @@ -515,6 +516,8 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread && context != null) throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); + syncContext = SynchronizationContext.Current; + Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); context?.Dispose(); @@ -553,6 +556,9 @@ namespace osu.Game.Database { factory.contextCreationLock.Release(); Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); + + // Post back to the update thread to revive any subscriptions. + syncContext?.Post(_ => ensureUpdateContext(), null); }); } diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index 84a54b208c..8d4fc5fc9f 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -33,10 +33,6 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings using (realmFactory.BlockAllOperations()) { } - - // retrieve context to revive realm subscriptions. - // TODO: should we do this from OsuGame or RealmContextFactory or something? Answer: yes. - var _ = realmFactory.Context; } }, }; From 81b07dee563b9ef01da5b8571061c09ccf0b417d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:41:33 +0300 Subject: [PATCH 051/587] Introduce `IExpandable` interface --- osu.Game/Overlays/IExpandable.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 osu.Game/Overlays/IExpandable.cs diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Overlays/IExpandable.cs new file mode 100644 index 0000000000..770ac97847 --- /dev/null +++ b/osu.Game/Overlays/IExpandable.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; + +namespace osu.Game.Overlays +{ + /// + /// An interface for drawables with ability to expand/contract. + /// + public interface IExpandable : IDrawable + { + /// + /// Whether this drawable is in an expanded state. + /// + BindableBool Expanded { get; } + } +} From 9946003069f7666f74bc383ac1371043cf6d0b35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jan 2022 05:09:40 +0900 Subject: [PATCH 052/587] Update osu.Game/Screens/Menu/IntroScreen.cs Co-authored-by: Salman Ahmed --- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index db9d9b83c4..10be90a5f0 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Menu int setCount = sets.Count; - if (sets.Any()) + if (setCount > 0) { var found = sets[RNG.Next(0, setCount - 1)].Beatmaps.FirstOrDefault(); From 955bab926fac76513a660737a42e48e01cfd9bc0 Mon Sep 17 00:00:00 2001 From: mk-56 Date: Sat, 22 Jan 2022 19:38:56 +0100 Subject: [PATCH 053/587] Separate the settings for each modes radiuses --- .../Mods/CatchModFlashlight.cs | 19 ++++++++-- .../Mods/ManiaModFlashlight.cs | 19 ++++++++-- .../Mods/OsuModFlashlight.cs | 36 ++++++++++++------- .../Mods/TaikoModFlashlight.cs | 19 ++++++++-- osu.Game/Rulesets/Mods/ModFlashlight.cs | 20 ++--------- 5 files changed, 75 insertions(+), 38 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index 9d9fa5aed4..f75772b04e 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -3,6 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Configuration; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Mods; @@ -15,8 +16,22 @@ namespace osu.Game.Rulesets.Catch.Mods { public override double ScoreMultiplier => 1.12; - public override bool DefaultComboDependency => true; - public override float DefaultRadius => 350; + [SettingSource("Change radius based on combo", "Decrease the flashlight radius as combo increases.")] + public override BindableBool ChangeRadius { get; } = new BindableBool + { + Default = true, + Value = true + }; + + [SettingSource("Initial radius", "Initial radius of the flashlight area.")] + public override BindableNumber InitialRadius { get; } = new BindableNumber + { + MinValue = 150f, + MaxValue = 600f, + Default = 350f, + Value = 350f, + Precision = 5f + }; public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value); diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index 4fff736c57..a6a3c3be73 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Layout; +using osu.Game.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; using osuTK; @@ -16,8 +17,22 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModHidden) }; - public override bool DefaultComboDependency => false; - public override float DefaultRadius => 180; + [SettingSource("Change radius based on combo", "Decrease the flashlight radius as combo increases.")] + public override BindableBool ChangeRadius { get; } = new BindableBool + { + Default = false, + Value = false + }; + + [SettingSource("Initial radius", "Initial radius of the flashlight area.")] + public override BindableNumber InitialRadius { get; } = new BindableNumber + { + MinValue = 0f, + MaxValue = 230f, + Default = 50f, + Value = 50f, + Precision = 5f + }; public override Flashlight CreateFlashlight() => new ManiaFlashlight(ChangeRadius.Value, InitialRadius.Value); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index f381d14ffe..e2a6d0f0dc 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -20,14 +20,32 @@ namespace osu.Game.Rulesets.Osu.Mods { public override double ScoreMultiplier => 1.12; - public override bool DefaultComboDependency => true; - - //private const float default_flashlight_size = 180; - public override float DefaultRadius => 180; - private const double default_follow_delay = 120; + [SettingSource("Follow delay", "Milliseconds until the flashlight reaches the cursor")] + public BindableNumber FollowDelay { get; } = new BindableDouble(default_follow_delay) + { + MinValue = default_follow_delay, + MaxValue = default_follow_delay * 10, + Precision = default_follow_delay, + }; + [SettingSource("Change radius based on combo", "Decrease the flashlight radius as combo increases.")] + public override BindableBool ChangeRadius { get; } = new BindableBool + { + Default = true, + Value = true + }; + + [SettingSource("Initial radius", "Initial radius of the flashlight area.")] + public override BindableNumber InitialRadius { get; } = new BindableNumber + { + MinValue = 90f, + MaxValue = 360f, + Default = 180f, + Value = 180f, + Precision = 5f + }; private OsuFlashlight flashlight; @@ -39,14 +57,6 @@ namespace osu.Game.Rulesets.Osu.Mods s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange; } - [SettingSource("Follow delay", "Milliseconds until the flashlight reaches the cursor")] - public BindableNumber FollowDelay { get; } = new BindableDouble(default_follow_delay) - { - MinValue = default_follow_delay, - MaxValue = default_follow_delay * 10, - Precision = default_follow_delay, - }; - private class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition { public double FollowDelay { private get; set; } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index 76f7c45b75..71c9d777ec 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -4,6 +4,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Layout; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; @@ -16,10 +17,22 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override double ScoreMultiplier => 1.12; - public override bool DefaultComboDependency => true; + [SettingSource("Change radius based on combo", "Decrease the flashlight radius as combo increases.")] + public override BindableBool ChangeRadius { get; } = new BindableBool + { + Default = true, + Value = true + }; - //private const float default_flashlight_size = 250; - public override float DefaultRadius => 250; + [SettingSource("Initial radius", "Initial radius of the flashlight area.")] + public override BindableNumber InitialRadius { get; } = new BindableNumber + { + MinValue = 0f, + MaxValue = 400f, + Default = 250f, + Value = 250f, + Precision = 5f + }; public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value); diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index c218ab45fe..51006d96e8 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -34,26 +34,10 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Restricted view area."; [SettingSource("Change radius based on combo", "Decrease the flashlight radius as combo increases.")] - public Bindable ChangeRadius { get; private set; } + public abstract BindableBool ChangeRadius { get; } [SettingSource("Initial radius", "Initial radius of the flashlight area.")] - public BindableNumber InitialRadius { get; private set; } - - public abstract float DefaultRadius { get; } - - public abstract bool DefaultComboDependency { get; } - - internal ModFlashlight() - { - InitialRadius = new BindableFloat(DefaultRadius) - { - MinValue = DefaultRadius * .5f, - MaxValue = DefaultRadius * 1.5f, - Precision = 5f, - }; - - ChangeRadius = new BindableBool(DefaultComboDependency); - } + public abstract BindableNumber InitialRadius { get; } } public abstract class ModFlashlight : ModFlashlight, IApplicableToDrawableRuleset, IApplicableToScoreProcessor From 735414bc49381df02d6fb243c25054ba8f35e204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Jan 2022 17:27:27 +0100 Subject: [PATCH 054/587] Replace `TimeSignatures` enum with struct for storage of arbitrary meter --- .../Skinning/Default/CirclePiece.cs | 2 +- .../Formats/LegacyBeatmapDecoderTest.cs | 6 +-- .../NonVisual/BarLineGeneratorTest.cs | 6 +-- .../TestSceneNightcoreBeatContainer.cs | 4 +- .../ControlPoints/TimingControlPoint.cs | 4 +- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 4 +- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/Timing/TimeSignature.cs | 45 +++++++++++++++++++ osu.Game/Beatmaps/Timing/TimeSignatures.cs | 4 +- osu.Game/Rulesets/Mods/Metronome.cs | 2 +- osu.Game/Rulesets/Mods/ModNightcore.cs | 10 ++--- osu.Game/Rulesets/Objects/BarLineGenerator.cs | 6 +-- .../Timeline/TimelineTickDisplay.cs | 2 +- .../RowAttributes/TimingRowAttribute.cs | 2 +- osu.Game/Screens/Edit/Timing/TimingSection.cs | 11 +++-- osu.Game/Screens/Menu/MenuSideFlashes.cs | 4 +- osu.Game/Screens/Menu/OsuLogo.cs | 2 +- 17 files changed, 84 insertions(+), 32 deletions(-) create mode 100644 osu.Game/Beatmaps/Timing/TimeSignature.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs index 8ca996159b..a106c4f629 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default if (!effectPoint.KiaiMode) return; - if (beatIndex % (int)timingPoint.TimeSignature != 0) + if (beatIndex % timingPoint.TimeSignature.Numerator != 0) return; double duration = timingPoint.BeatLength * 2; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 6ec14e6351..0459753b28 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -178,17 +178,17 @@ namespace osu.Game.Tests.Beatmaps.Formats var timingPoint = controlPoints.TimingPointAt(0); Assert.AreEqual(956, timingPoint.Time); Assert.AreEqual(329.67032967033, timingPoint.BeatLength); - Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature); + Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature); timingPoint = controlPoints.TimingPointAt(48428); Assert.AreEqual(956, timingPoint.Time); Assert.AreEqual(329.67032967033d, timingPoint.BeatLength); - Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature); + Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature); timingPoint = controlPoints.TimingPointAt(119637); Assert.AreEqual(119637, timingPoint.Time); Assert.AreEqual(659.340659340659, timingPoint.BeatLength); - Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature); + Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature); var difficultyPoint = controlPoints.DifficultyPointAt(0); Assert.AreEqual(0, difficultyPoint.Time); diff --git a/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs index 834c05fd08..6ae8231deb 100644 --- a/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs +++ b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.NonVisual const int beat_length_numerator = 2000; const int beat_length_denominator = 7; - const TimeSignatures signature = TimeSignatures.SimpleQuadruple; + TimeSignature signature = TimeSignature.SimpleQuadruple; var beatmap = new Beatmap { @@ -49,7 +49,7 @@ namespace osu.Game.Tests.NonVisual for (int i = 0; i * beat_length_denominator < barLines.Count; i++) { var barLine = barLines[i * beat_length_denominator]; - int expectedTime = beat_length_numerator * (int)signature * i; + int expectedTime = beat_length_numerator * signature.Numerator * i; // every seventh bar's start time should be at least greater than the whole number we expect. // It cannot be less, as that can affect overlapping scroll algorithms @@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual Assert.IsTrue(Precision.AlmostEquals(barLine.StartTime, expectedTime)); // check major/minor lines for good measure too - Assert.AreEqual(i % (int)signature == 0, barLine.Major); + Assert.AreEqual(i % signature.Numerator == 0, barLine.Major); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs index 951ee1489d..759e4fa4ec 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs @@ -24,8 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay Add(new ModNightcore.NightcoreBeatContainer()); - AddStep("change signature to quadruple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleQuadruple)); - AddStep("change signature to triple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleTriple)); + AddStep("change signature to quadruple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignature.SimpleQuadruple)); + AddStep("change signature to triple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignature.SimpleTriple)); } } } diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index ec20328fab..922439fcb8 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time signature at this control point. /// - public readonly Bindable TimeSignatureBindable = new Bindable(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple }; + public readonly Bindable TimeSignatureBindable = new Bindable(TimeSignature.SimpleQuadruple); /// /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing. @@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time signature at this control point. /// - public TimeSignatures TimeSignature + public TimeSignature TimeSignature { get => TimeSignatureBindable.Value; set => TimeSignatureBindable.Value = value; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 893eb8ab78..8f3f05aa9f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -340,9 +340,9 @@ namespace osu.Game.Beatmaps.Formats double beatLength = Parsing.ParseDouble(split[1].Trim()); double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; - TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple; + TimeSignature timeSignature = TimeSignature.SimpleQuadruple; if (split.Length >= 3) - timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)Parsing.ParseInt(split[2]); + timeSignature = split[2][0] == '0' ? TimeSignature.SimpleQuadruple : new TimeSignature(Parsing.ParseInt(split[2])); LegacySampleBank sampleSet = defaultSampleBank; if (split.Length >= 4) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index ebdc882d2f..4cf6d3335f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -227,7 +227,7 @@ namespace osu.Game.Beatmaps.Formats if (effectPoint.OmitFirstBarLine) effectFlags |= LegacyEffectFlags.OmitFirstBarLine; - writer.Write(FormattableString.Invariant($"{(int)legacyControlPoints.TimingPointAt(time).TimeSignature},")); + writer.Write(FormattableString.Invariant($"{legacyControlPoints.TimingPointAt(time).TimeSignature.Numerator},")); writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},")); writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); diff --git a/osu.Game/Beatmaps/Timing/TimeSignature.cs b/osu.Game/Beatmaps/Timing/TimeSignature.cs new file mode 100644 index 0000000000..5bfeea5e9b --- /dev/null +++ b/osu.Game/Beatmaps/Timing/TimeSignature.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Beatmaps.Timing +{ + /// + /// Stores the time signature of a track. + /// For now, the lower numeral can only be 4; support for other denominators can be considered at a later date. + /// + public class TimeSignature : IEquatable + { + /// + /// The numerator of a signature. + /// + public int Numerator { get; } + + // TODO: support time signatures with a denominator other than 4 + // this in particular requires a new beatmap format. + + public TimeSignature(int numerator) + { + if (numerator < 1) + throw new ArgumentOutOfRangeException(nameof(numerator), numerator, "The numerator of a time signature must be positive."); + + Numerator = numerator; + } + + public static TimeSignature SimpleTriple => new TimeSignature(3); + public static TimeSignature SimpleQuadruple => new TimeSignature(4); + + public override string ToString() => $"{Numerator}/4"; + + public bool Equals(TimeSignature other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Numerator == other.Numerator; + } + + public override int GetHashCode() => Numerator; + } +} diff --git a/osu.Game/Beatmaps/Timing/TimeSignatures.cs b/osu.Game/Beatmaps/Timing/TimeSignatures.cs index 33e6342ae6..d783d3f9ec 100644 --- a/osu.Game/Beatmaps/Timing/TimeSignatures.cs +++ b/osu.Game/Beatmaps/Timing/TimeSignatures.cs @@ -1,11 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.ComponentModel; namespace osu.Game.Beatmaps.Timing { - public enum TimeSignatures + [Obsolete("Use osu.Game.Beatmaps.Timing.TimeSignature instead.")] + public enum TimeSignatures // can be removed 20220722 { [Description("4/4")] SimpleQuadruple = 4, diff --git a/osu.Game/Rulesets/Mods/Metronome.cs b/osu.Game/Rulesets/Mods/Metronome.cs index 8b6d86c45f..b85a341577 100644 --- a/osu.Game/Rulesets/Mods/Metronome.cs +++ b/osu.Game/Rulesets/Mods/Metronome.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mods if (!IsBeatSyncedWithTrack) return; - int timeSignature = (int)timingPoint.TimeSignature; + int timeSignature = timingPoint.TimeSignature.Numerator; // play metronome from one measure before the first object. if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index a44967c21c..993efead33 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Mods { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - int beatsPerBar = (int)timingPoint.TimeSignature; + int beatsPerBar = timingPoint.TimeSignature.Numerator; int segmentLength = beatsPerBar * Divisor * bars_per_segment; if (!IsBeatSyncedWithTrack) @@ -102,14 +102,14 @@ namespace osu.Game.Rulesets.Mods playBeatFor(beatIndex % segmentLength, timingPoint.TimeSignature); } - private void playBeatFor(int beatIndex, TimeSignatures signature) + private void playBeatFor(int beatIndex, TimeSignature signature) { if (beatIndex == 0) finishSample?.Play(); - switch (signature) + switch (signature.Numerator) { - case TimeSignatures.SimpleTriple: + case 3: switch (beatIndex % 6) { case 0: @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Mods break; - case TimeSignatures.SimpleQuadruple: + case 4: switch (beatIndex % 4) { case 0: diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs index e78aa5a5a0..d71a499119 100644 --- a/osu.Game/Rulesets/Objects/BarLineGenerator.cs +++ b/osu.Game/Rulesets/Objects/BarLineGenerator.cs @@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Objects int currentBeat = 0; // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object - double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * (int)currentTimingPoint.TimeSignature; + double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator; - double barLength = currentTimingPoint.BeatLength * (int)currentTimingPoint.TimeSignature; + double barLength = currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator; for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++) { @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Objects BarLines.Add(new TBarLine { StartTime = t, - Major = currentBeat % (int)currentTimingPoint.TimeSignature == 0 + Major = currentBeat % currentTimingPoint.TimeSignature.Numerator == 0 }); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 1415014e59..cc4041394d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -125,7 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (beat == 0 && i == 0) nextMinTick = float.MinValue; - int indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value); + int indexInBar = beat % (point.TimeSignature.Numerator * beatDivisor.Value); int divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); var colour = BindableBeatDivisor.GetColourFor(divisor, colours); diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs index ab840e56a7..f8ec4aef25 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes public class TimingRowAttribute : RowAttribute { private readonly BindableNumber beatLength; - private readonly Bindable timeSignature; + private readonly Bindable timeSignature; private OsuSpriteText text; public TimingRowAttribute(TimingControlPoint timing) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index a0bb9ac506..e0b09ce980 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Edit.Timing internal class TimingSection : Section { private SettingsSlider bpmSlider; - private SettingsEnumDropdown timeSignature; + private SettingsDropdown timeSignature; private BPMTextBox bpmTextEntry; [BackgroundDependencyLoader] @@ -25,9 +25,14 @@ namespace osu.Game.Screens.Edit.Timing { bpmTextEntry = new BPMTextBox(), bpmSlider = new BPMSlider(), - timeSignature = new SettingsEnumDropdown + timeSignature = new SettingsDropdown { - LabelText = "Time Signature" + LabelText = "Time Signature", + Items = new[] + { + TimeSignature.SimpleTriple, + TimeSignature.SimpleQuadruple + } }, }); } diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index bdcd3020f8..cd0c75c1a1 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -94,9 +94,9 @@ namespace osu.Game.Screens.Menu if (beatIndex < 0) return; - if (effectPoint.KiaiMode ? beatIndex % 2 == 0 : beatIndex % (int)timingPoint.TimeSignature == 0) + if (effectPoint.KiaiMode ? beatIndex % 2 == 0 : beatIndex % timingPoint.TimeSignature.Numerator == 0) flash(leftBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes); - if (effectPoint.KiaiMode ? beatIndex % 2 == 1 : beatIndex % (int)timingPoint.TimeSignature == 0) + if (effectPoint.KiaiMode ? beatIndex % 2 == 1 : beatIndex % timingPoint.TimeSignature.Numerator == 0) flash(rightBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes); } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index f9388097ac..c82efe2d32 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -282,7 +282,7 @@ namespace osu.Game.Screens.Menu { this.Delay(early_activation).Schedule(() => { - if (beatIndex % (int)timingPoint.TimeSignature == 0) + if (beatIndex % timingPoint.TimeSignature.Numerator == 0) sampleDownbeat.Play(); else sampleBeat.Play(); From f39f2c93b52526a3e5d5aa06e18d0f0ac85020c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Jan 2022 18:54:14 +0100 Subject: [PATCH 055/587] Add control for arbitrary-numerator time signatures --- .../Editing/TestSceneLabelledTimeSignature.cs | 88 ++++++++++++++++++ .../Edit/Timing/LabelledTimeSignature.cs | 93 +++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs create mode 100644 osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs b/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs new file mode 100644 index 0000000000..cedbb5025f --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.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.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit.Timing; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneLabelledTimeSignature : OsuManualInputManagerTestScene + { + private LabelledTimeSignature timeSignature; + + private void createLabelledTimeSignature(TimeSignature initial) => AddStep("create labelled time signature", () => + { + Child = timeSignature = new LabelledTimeSignature + { + Label = "Time Signature", + RelativeSizeAxes = Axes.None, + Width = 400, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { Value = initial } + }; + }); + + private OsuTextBox numeratorTextBox => timeSignature.ChildrenOfType().Single(); + + [Test] + public void TestInitialValue() + { + createLabelledTimeSignature(TimeSignature.SimpleTriple); + AddAssert("current is 3/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleTriple)); + } + + [Test] + public void TestChangeViaCurrent() + { + createLabelledTimeSignature(TimeSignature.SimpleQuadruple); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + + AddStep("set current to 5/4", () => timeSignature.Current.Value = new TimeSignature(5)); + + AddAssert("current is 5/4", () => timeSignature.Current.Value.Equals(new TimeSignature(5))); + AddAssert("numerator is 5", () => numeratorTextBox.Current.Value == "5"); + + AddStep("set current to 3/4", () => timeSignature.Current.Value = TimeSignature.SimpleTriple); + + AddAssert("current is 3/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleTriple)); + AddAssert("numerator is 5", () => numeratorTextBox.Current.Value == "3"); + } + + [Test] + public void TestChangeNumerator() + { + createLabelledTimeSignature(TimeSignature.SimpleQuadruple); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + + AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox)); + + AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7"); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + + AddStep("drop focus", () => InputManager.ChangeFocus(null)); + AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7))); + } + + [Test] + public void TestInvalidChangeRollbackOnCommit() + { + createLabelledTimeSignature(TimeSignature.SimpleQuadruple); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + + AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox)); + + AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0"); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + + AddStep("drop focus", () => InputManager.ChangeFocus(null)); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4"); + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs b/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs new file mode 100644 index 0000000000..52aece75ad --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Screens.Edit.Timing +{ + internal class LabelledTimeSignature : LabelledComponent + { + public LabelledTimeSignature() + : base(false) + { + } + + protected override TimeSignatureBox CreateComponent() => new TimeSignatureBox(); + + internal class TimeSignatureBox : CompositeDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(TimeSignature.SimpleQuadruple); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private OsuNumberBox numeratorBox; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + numeratorBox = new OsuNumberBox + { + Width = 40, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + CornerRadius = CORNER_RADIUS, + CommitOnFocusLost = true + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding(10), + Text = "/4", + Font = OsuFont.Default.With(size: 20) + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateFromCurrent(), true); + numeratorBox.OnCommit += (_, __) => updateFromNumeratorBox(); + } + + private void updateFromCurrent() + { + numeratorBox.Current.Value = Current.Value.Numerator.ToString(); + } + + private void updateFromNumeratorBox() + { + if (int.TryParse(numeratorBox.Current.Value, out int numerator) && numerator > 0) + Current.Value = new TimeSignature(numerator); + else + { + // trigger `Current` change to restore the numerator box's text to a valid value. + Current.TriggerChange(); + } + } + } + } +} From 54f7b1b8d0cec1638c67492fadf7c7fca8577f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Jan 2022 18:54:28 +0100 Subject: [PATCH 056/587] Use new time signature control on timing screen --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index e0b09ce980..cd0b56d338 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Timing; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays.Settings; @@ -15,7 +14,7 @@ namespace osu.Game.Screens.Edit.Timing internal class TimingSection : Section { private SettingsSlider bpmSlider; - private SettingsDropdown timeSignature; + private LabelledTimeSignature timeSignature; private BPMTextBox bpmTextEntry; [BackgroundDependencyLoader] @@ -25,15 +24,10 @@ namespace osu.Game.Screens.Edit.Timing { bpmTextEntry = new BPMTextBox(), bpmSlider = new BPMSlider(), - timeSignature = new SettingsDropdown + timeSignature = new LabelledTimeSignature { - LabelText = "Time Signature", - Items = new[] - { - TimeSignature.SimpleTriple, - TimeSignature.SimpleQuadruple - } - }, + Label = "Time Signature" + } }); } From 62a2bccd7655f78a66d74a7d77d4dec73f311c3a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:41:45 +0300 Subject: [PATCH 057/587] Abstractify expansion logic from `ExpandingButtonContainer` --- osu.Game/Overlays/ExpandingButtonContainer.cs | 136 ++---------------- .../Overlays/ExpandingControlContainer.cs | 124 ++++++++++++++++ osu.Game/Overlays/IExpandingContainer.cs | 16 +++ osu.Game/Overlays/SettingsPanel.cs | 2 +- 4 files changed, 150 insertions(+), 128 deletions(-) create mode 100644 osu.Game/Overlays/ExpandingControlContainer.cs create mode 100644 osu.Game/Overlays/IExpandingContainer.cs diff --git a/osu.Game/Overlays/ExpandingButtonContainer.cs b/osu.Game/Overlays/ExpandingButtonContainer.cs index 4eb8c47a1f..d7ff285707 100644 --- a/osu.Game/Overlays/ExpandingButtonContainer.cs +++ b/osu.Game/Overlays/ExpandingButtonContainer.cs @@ -1,141 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Linq; -using osu.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; -using osu.Framework.Testing; -using osu.Framework.Threading; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osuTK; namespace osu.Game.Overlays { - public abstract class ExpandingButtonContainer : Container, IStateful + /// + /// An with a long hover expansion delay for buttons. + /// + /// + /// Mostly used for buttons with explanatory labels, in which the label would display after a "long hover". + /// + public class ExpandingButtonContainer : ExpandingControlContainer { - private readonly float contractedWidth; - private readonly float expandedWidth; - - public event Action StateChanged; - - protected override Container Content => FillFlow; - - protected FillFlowContainer FillFlow { get; } - protected ExpandingButtonContainer(float contractedWidth, float expandedWidth) + : base(contractedWidth, expandedWidth) { - this.contractedWidth = contractedWidth; - this.expandedWidth = expandedWidth; - - RelativeSizeAxes = Axes.Y; - Width = contractedWidth; - - InternalChildren = new Drawable[] - { - new SidebarScrollContainer - { - Children = new[] - { - FillFlow = new FillFlowContainer - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - } - } - }, - }; } - private ScheduledDelegate expandEvent; - private ExpandedState state; - - protected override bool OnHover(HoverEvent e) - { - queueExpandIfHovering(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - expandEvent?.Cancel(); - hoveredButton = null; - State = ExpandedState.Contracted; - - base.OnHoverLost(e); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - queueExpandIfHovering(); - return base.OnMouseMove(e); - } - - private class SidebarScrollContainer : OsuScrollContainer - { - public SidebarScrollContainer() - { - RelativeSizeAxes = Axes.Both; - ScrollbarVisible = false; - } - } - - public ExpandedState State - { - get => state; - set - { - expandEvent?.Cancel(); - - if (state == value) return; - - state = value; - - switch (state) - { - default: - this.ResizeTo(new Vector2(contractedWidth, Height), 500, Easing.OutQuint); - break; - - case ExpandedState.Expanded: - this.ResizeTo(new Vector2(expandedWidth, Height), 500, Easing.OutQuint); - break; - } - - StateChanged?.Invoke(State); - } - } - - private Drawable hoveredButton; - - private void queueExpandIfHovering() - { - // if the same button is hovered, let the scheduled expand play out.. - if (hoveredButton?.IsHovered == true) - return; - - // ..otherwise check whether a new button is hovered, and if so, queue a new hover operation. - - // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way - // to handle cases like the editor where the buttons may be nested within a child hierarchy. - hoveredButton = FillFlow.ChildrenOfType().FirstOrDefault(c => c.IsHovered); - - expandEvent?.Cancel(); - - if (hoveredButton?.IsHovered == true && State != ExpandedState.Expanded) - expandEvent = Scheduler.AddDelayed(() => State = ExpandedState.Expanded, 750); - } - } - - public enum ExpandedState - { - Contracted, - Expanded, + protected override double HoverExpansionDelay => 750; } } diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs new file mode 100644 index 0000000000..8e02cab923 --- /dev/null +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -0,0 +1,124 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Framework.Testing; +using osu.Framework.Threading; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Overlays +{ + /// + /// Represents a with the ability to expand/contract when hovering the controls within it. + /// + /// The type of UI control to lookup for hover expansion. + public class ExpandingControlContainer : Container, IExpandingContainer + where TControl : class, IDrawable + { + private readonly float contractedWidth; + private readonly float expandedWidth; + + public BindableBool Expanded { get; } = new BindableBool(); + + /// + /// Delay before the container switches to expanded state from hover. + /// + protected virtual double HoverExpansionDelay => 0; + + protected override Container Content => FillFlow; + + protected FillFlowContainer FillFlow { get; } + + protected ExpandingControlContainer(float contractedWidth, float expandedWidth) + { + this.contractedWidth = contractedWidth; + this.expandedWidth = expandedWidth; + + RelativeSizeAxes = Axes.Y; + Width = contractedWidth; + + InternalChild = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = FillFlow = new FillFlowContainer + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }, + }; + } + + private ScheduledDelegate hoverExpandEvent; + private TControl activeControl; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(v => + { + this.ResizeWidthTo(v.NewValue ? expandedWidth : contractedWidth, 500, Easing.OutQuint); + }, true); + } + + protected override void Update() + { + base.Update(); + + // if the container was expanded from hovering over a control, we have to check per-frame whether we can contract it back. + // that's because contracting the container depends not only on whether it's no longer hovered, + // but also on whether the hovered control is no longer in a dragged state (if it was). + if (hoverExpandEvent != null && !IsHovered && (activeControl == null || !isControlActive(activeControl))) + { + hoverExpandEvent?.Cancel(); + + Expanded.Value = false; + hoverExpandEvent = null; + activeControl = null; + } + } + + protected override bool OnHover(HoverEvent e) + { + queueExpandIfHovering(); + return true; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + queueExpandIfHovering(); + return base.OnMouseMove(e); + } + + private void queueExpandIfHovering() + { + // if the same control is hovered or dragged, let the scheduled expand play out.. + if (activeControl != null && isControlActive(activeControl)) + return; + + // ..otherwise check whether a new control is hovered, and if so, queue a new hover operation. + hoverExpandEvent?.Cancel(); + + // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way + // to handle cases like the editor where the controls may be nested within a child hierarchy. + activeControl = FillFlow.ChildrenOfType().FirstOrDefault(isControlActive); + + if (activeControl != null && !Expanded.Value) + hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); + } + + /// + /// Whether the given control is currently active, by checking whether it's hovered or dragged. + /// + private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged; + } +} diff --git a/osu.Game/Overlays/IExpandingContainer.cs b/osu.Game/Overlays/IExpandingContainer.cs new file mode 100644 index 0000000000..ec5f0c90f4 --- /dev/null +++ b/osu.Game/Overlays/IExpandingContainer.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Overlays +{ + /// + /// A target expanding container that should be resolved by children s to propagate state changes. + /// + [Cached(typeof(IExpandingContainer))] + public interface IExpandingContainer : IContainer, IExpandable + { + } +} diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index ba7118cffe..b11b6fde27 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -265,7 +265,7 @@ namespace osu.Game.Overlays return; SectionsContainer.ScrollTo(section); - Sidebar.State = ExpandedState.Contracted; + Sidebar.Expanded.Value = false; }, }; } From 326f12f8477da2838a368e921329d1ee726e4c44 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:43:05 +0300 Subject: [PATCH 058/587] Add `IExpandable` support for `SettingsToolboxGroup` --- osu.Game/Overlays/SettingsToolboxGroup.cs | 80 +++++++++++-------- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 2 +- 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index ca0980a9c9..9e7223df9d 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; @@ -18,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public abstract class SettingsToolboxGroup : Container + public class SettingsToolboxGroup : Container, IExpandable { private const float transition_duration = 250; private const int container_width = 270; @@ -34,30 +35,7 @@ namespace osu.Game.Overlays private readonly FillFlowContainer content; private readonly IconButton button; - private bool expanded = true; - - public bool Expanded - { - get => expanded; - set - { - if (expanded == value) return; - - expanded = value; - - content.ClearTransforms(); - - if (expanded) - content.AutoSizeAxes = Axes.Y; - else - { - content.AutoSizeAxes = Axes.None; - content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); - } - - updateExpanded(); - } - } + public BindableBool Expanded { get; } = new BindableBool(true); private Color4 expandedColour; @@ -67,7 +45,7 @@ namespace osu.Game.Overlays /// Create a new instance. /// /// The title to be displayed in the header of this group. - protected SettingsToolboxGroup(string title) + public SettingsToolboxGroup(string title) { AutoSizeAxes = Axes.Y; Width = container_width; @@ -115,7 +93,7 @@ namespace osu.Game.Overlays Position = new Vector2(-15, 0), Icon = FontAwesome.Solid.Bars, Scale = new Vector2(0.75f), - Action = () => Expanded = !Expanded, + Action = () => Expanded.Toggle(), }, } }, @@ -155,23 +133,58 @@ namespace osu.Game.Overlays headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); } + [Resolved(canBeNull: true)] + private IExpandingContainer expandingContainer { get; set; } + + private bool expandedByContainer; + protected override void LoadComplete() { base.LoadComplete(); - this.Delay(600).FadeTo(inactive_alpha, fade_duration, Easing.OutQuint); - updateExpanded(); + expandingContainer?.Expanded.BindValueChanged(containerExpanded => + { + if (containerExpanded.NewValue && !Expanded.Value) + { + Expanded.Value = true; + expandedByContainer = true; + } + else if (!containerExpanded.NewValue && expandedByContainer) + { + Expanded.Value = false; + expandedByContainer = false; + } + + updateActiveState(); + }, true); + + Expanded.BindValueChanged(v => + { + content.ClearTransforms(); + + if (v.NewValue) + content.AutoSizeAxes = Axes.Y; + else + { + content.AutoSizeAxes = Axes.None; + content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); + } + + button.FadeColour(Expanded.Value ? expandedColour : Color4.White, 200, Easing.InOutQuint); + }, true); + + this.Delay(600).Schedule(updateActiveState); } protected override bool OnHover(HoverEvent e) { - this.FadeIn(fade_duration, Easing.OutQuint); + updateActiveState(); return false; } protected override void OnHoverLost(HoverLostEvent e) { - this.FadeTo(inactive_alpha, fade_duration, Easing.OutQuint); + updateActiveState(); base.OnHoverLost(e); } @@ -181,7 +194,10 @@ namespace osu.Game.Overlays expandedColour = colours.Yellow; } - private void updateExpanded() => button.FadeColour(expanded ? expandedColour : Color4.White, 200, Easing.InOutQuint); + private void updateActiveState() + { + this.FadeTo(IsHovered || expandingContainer?.Expanded.Value == true ? 1 : inactive_alpha, fade_duration, Easing.OutQuint); + } protected override Container Content => content; diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index ffcbb06fb3..807b4989c7 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play.HUD //CollectionSettings = new CollectionSettings(), //DiscussionSettings = new DiscussionSettings(), PlaybackSettings = new PlaybackSettings(), - VisualSettings = new VisualSettings { Expanded = false } + VisualSettings = new VisualSettings { Expanded = { Value = false } } } }; } From f4c7a332c343b1d23df835914150fe85992ef7ee Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:45:08 +0300 Subject: [PATCH 059/587] Add `IExpandable` support for `SettingsItem` --- osu.Game/Overlays/Settings/ISettingsItem.cs | 9 +- osu.Game/Overlays/Settings/SettingsItem.cs | 91 ++++++++++++++++----- 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index e7afa48502..fe21f0664a 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -2,12 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Graphics; namespace osu.Game.Overlays.Settings { - public interface ISettingsItem : IDrawable, IDisposable + /// + /// A non-generic interface for s. + /// + public interface ISettingsItem : IExpandable, IDisposable { + /// + /// Invoked when the setting value has changed. + /// event Action SettingChanged; } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index e709be1343..cc8d5b36d0 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Settings protected readonly FillFlowContainer FlowContent; - private SpriteText labelText; + private SpriteText label; private OsuTextFlowContainer warningText; @@ -42,21 +42,34 @@ namespace osu.Game.Overlays.Settings [Resolved] private OsuColour colours { get; set; } + private LocalisableString labelText; + public virtual LocalisableString LabelText { - get => labelText?.Text ?? string.Empty; + get => labelText; set { - if (labelText == null) - { - // construct lazily for cases where the label is not needed (may be provided by the Control). - FlowContent.Insert(-1, labelText = new OsuSpriteText()); + ensureLabelCreated(); - updateDisabled(); - } + labelText = value; + updateLabelText(); + } + } - labelText.Text = value; - updateLayout(); + private LocalisableString? contractedLabelText; + + /// + /// Text to be displayed in place of when this is in a contracted state. + /// + public LocalisableString? ContractedLabelText + { + get => contractedLabelText; + set + { + ensureLabelCreated(); + + contractedLabelText = value; + updateLabelText(); } } @@ -90,6 +103,10 @@ namespace osu.Game.Overlays.Settings set => controlWithCurrent.Current = value; } + public BindableBool Expanded { get; } = new BindableBool(true); + + public event Action SettingChanged; + public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List(Keywords) { LabelText.ToString() }.ToArray(); public IEnumerable Keywords { get; set; } @@ -101,8 +118,6 @@ namespace osu.Game.Overlays.Settings public bool FilteringActive { get; set; } - public event Action SettingChanged; - protected SettingsItem() { RelativeSizeAxes = Axes.X; @@ -151,23 +166,59 @@ namespace osu.Game.Overlays.Settings Anchor = Anchor.Centre, Origin = Anchor.Centre }); - updateLayout(); } } - private void updateLayout() - { - bool hasLabel = labelText != null && !string.IsNullOrEmpty(labelText.Text.ToString()); + [Resolved(canBeNull: true)] + private IExpandingContainer expandingContainer { get; set; } - // if the settings item is providing a label, the default value indicator should be centred vertically to the left of the label. + protected override void LoadComplete() + { + base.LoadComplete(); + + expandingContainer?.Expanded.BindValueChanged(containerExpanded => Expanded.Value = containerExpanded.NewValue, true); + + Expanded.BindValueChanged(v => + { + updateLabelText(); + + Control.FadeTo(v.NewValue ? 1 : 0, 500, Easing.OutQuint); + Control.BypassAutoSizeAxes = v.NewValue ? Axes.None : Axes.Both; + }, true); + + FinishTransforms(true); + } + + private void ensureLabelCreated() + { + if (label != null) + return; + + // construct lazily for cases where the label is not needed (may be provided by the Control). + FlowContent.Insert(-1, label = new OsuSpriteText()); + + updateDisabled(); + } + + private void updateLabelText() + { + if (label != null) + { + if (contractedLabelText is LocalisableString contractedText) + label.Text = Expanded.Value ? labelText : contractedText; + else + label.Text = labelText; + } + + // if the settings item is providing a non-empty label, the default value indicator should be centred vertically to the left of the label. // otherwise, it should be centred vertically to the left of the main control of the settings item. - defaultValueIndicatorContainer.Height = hasLabel ? labelText.DrawHeight : Control.DrawHeight; + defaultValueIndicatorContainer.Height = !string.IsNullOrEmpty(label?.Text.ToString()) ? label.DrawHeight : Control.DrawHeight; } private void updateDisabled() { - if (labelText != null) - labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; + if (label != null) + label.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } } } From 648e7f6bbc4883d591ed7c01994f511006caa770 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:47:11 +0300 Subject: [PATCH 060/587] Handle controls with dragging state wrapped in underlying components I'm not 100% sure about this approach but it'll do for now. --- osu.Game/Overlays/ExpandingControlContainer.cs | 2 +- osu.Game/Overlays/Settings/ISettingsItem.cs | 5 +++++ osu.Game/Overlays/Settings/SettingsItem.cs | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs index 8e02cab923..2accd63fb9 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -119,6 +119,6 @@ namespace osu.Game.Overlays /// /// Whether the given control is currently active, by checking whether it's hovered or dragged. /// - private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged; + private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is ISettingsItem item && item.IsControlDragged); } } diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index fe21f0664a..20e2f48f96 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -14,5 +14,10 @@ namespace osu.Game.Overlays.Settings /// Invoked when the setting value has changed. /// event Action SettingChanged; + + /// + /// Returns whether the UI control is currently in a dragged state. + /// + bool IsControlDragged { get; } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index cc8d5b36d0..29980dc5a8 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -105,6 +105,8 @@ namespace osu.Game.Overlays.Settings public BindableBool Expanded { get; } = new BindableBool(true); + public bool IsControlDragged => Control.IsDragged; + public event Action SettingChanged; public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List(Keywords) { LabelText.ToString() }.ToArray(); From 6b35c0fe01795d664fc3599085a4ec2aa17d614d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 18:39:58 +0300 Subject: [PATCH 061/587] Add test scene for `ExpandingControlContainer` --- .../TestSceneExpandingControlContainer.cs | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs new file mode 100644 index 0000000000..d75089ceac --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs @@ -0,0 +1,183 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneExpandingControlContainer : OsuManualInputManagerTestScene + { + private TestExpandingContainer container; + private SettingsToolboxGroup toolboxGroup; + + private SettingsSlider slider1; + private SettingsSlider slider2; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = container = new TestExpandingContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Height = 0.33f, + Child = toolboxGroup = new SettingsToolboxGroup("sliders") + { + RelativeSizeAxes = Axes.X, + Width = 1, + Children = new Drawable[] + { + slider1 = new SettingsSlider + { + Current = new BindableFloat + { + Default = 1.0f, + MinValue = 1.0f, + MaxValue = 10.0f, + Precision = 0.01f, + }, + }, + slider2 = new SettingsSlider + { + Current = new BindableDouble + { + Default = 1.0, + MinValue = 1.0, + MaxValue = 10.0, + Precision = 0.01, + }, + }, + } + } + }; + + slider1.Current.BindValueChanged(v => + { + slider1.LabelText = $"Slider One ({v.NewValue:0.##x})"; + slider1.ContractedLabelText = $"S. 1. ({v.NewValue:0.##x})"; + }, true); + + slider2.Current.BindValueChanged(v => + { + slider2.LabelText = $"Slider Two ({v.NewValue:N2})"; + slider2.ContractedLabelText = $"S. 2. ({v.NewValue:N2})"; + }, true); + }); + + [Test] + public void TestDisplay() + { + AddStep("switch to contracted", () => container.Expanded.Value = false); + AddStep("switch to expanded", () => container.Expanded.Value = true); + AddStep("set left origin", () => container.Origin = Anchor.CentreLeft); + AddStep("set centre origin", () => container.Origin = Anchor.Centre); + AddStep("set right origin", () => container.Origin = Anchor.CentreRight); + } + + /// + /// Tests hovering over controls expands the parenting container appropriately and does not contract until hover is lost from container. + /// + [Test] + public void TestHoveringControlExpandsContainer() + { + AddAssert("ensure container contracted", () => !container.Expanded.Value); + + AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); + AddAssert("container expanded", () => container.Expanded.Value); + AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("hover group top", () => InputManager.MoveMouseTo(toolboxGroup.ScreenSpaceDrawQuad.TopLeft + new Vector2(5))); + AddAssert("container still expanded", () => container.Expanded.Value); + AddAssert("controls still expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("container contracted", () => !container.Expanded.Value); + AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); + } + + /// + /// Tests dragging a UI control (e.g. ) outside its parenting container does not contract it until dragging is finished. + /// + [Test] + public void TestDraggingControlOutsideDoesntContractContainer() + { + AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); + AddAssert("container expanded", () => container.Expanded.Value); + + AddStep("hover slider nub", () => InputManager.MoveMouseTo(slider1.ChildrenOfType().Single())); + AddStep("hold slider nub", () => InputManager.PressButton(MouseButton.Left)); + AddStep("drag outside container", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("container still expanded", () => container.Expanded.Value); + + AddStep("release slider nub", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("container contracted", () => !container.Expanded.Value); + } + + /// + /// Tests expanding a container will expand underlying groups if contracted. + /// + [Test] + public void TestExpandingContainerExpandsContractedGroup() + { + AddStep("contract group", () => toolboxGroup.Expanded.Value = false); + + AddStep("expand container", () => container.Expanded.Value = true); + AddAssert("group expanded", () => toolboxGroup.Expanded.Value); + AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("contract container", () => container.Expanded.Value = false); + AddAssert("group contracted", () => !toolboxGroup.Expanded.Value); + AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); + } + + /// + /// Tests contracting a container does not contract underlying groups if expanded by user (i.e. by setting directly). + /// + [Test] + public void TestContractingContainerDoesntContractUserExpandedGroup() + { + AddAssert("ensure group expanded", () => toolboxGroup.Expanded.Value); + + AddStep("expand container", () => container.Expanded.Value = true); + AddAssert("group still expanded", () => toolboxGroup.Expanded.Value); + AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("contract container", () => container.Expanded.Value = false); + AddAssert("group still expanded", () => toolboxGroup.Expanded.Value); + AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); + } + + /// + /// Tests expanding a container via does not get contracted by losing hover. + /// + [Test] + public void TestExpandingContainerDoesntGetContractedByHover() + { + AddStep("expand container", () => container.Expanded.Value = true); + + AddStep("hover control", () => InputManager.MoveMouseTo(slider1)); + AddAssert("container still expanded", () => container.Expanded.Value); + + AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("container still expanded", () => container.Expanded.Value); + } + + private class TestExpandingContainer : ExpandingControlContainer + { + public TestExpandingContainer() + : base(120, 250) + { + } + } + } +} From 48aa1677dcd28e0477bb84ae17075ba53d46e56e Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 23 Jan 2022 11:01:30 +0800 Subject: [PATCH 062/587] Include hit results of nested hit objects in statistics of perfect score --- .../Difficulty/PerformanceBreakdownCalculator.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 5cf63d0102..46342b237c 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -64,7 +66,7 @@ namespace osu.Game.Rulesets.Difficulty // create statistics assuming all hit objects have perfect hit result var statistics = beatmap.HitObjects - .Select(ho => ho.CreateJudgement().MaxResult) + .SelectMany(getPerfectHitResults) .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) .ToDictionary(pair => pair.hitResult, pair => pair.count); perfectPlay.Statistics = statistics; @@ -89,5 +91,13 @@ namespace osu.Game.Rulesets.Difficulty return ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate(); }, cancellationToken); } + + private IEnumerable getPerfectHitResults(HitObject hitObject) + { + foreach (HitObject nested in hitObject.NestedHitObjects) + yield return nested.CreateJudgement().MaxResult; + + yield return hitObject.CreateJudgement().MaxResult; + } } } From 1ea5a2e6a75301341355f7a934b6b79a5f282d7c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 23 Jan 2022 10:11:07 +0300 Subject: [PATCH 063/587] Fix incorrect assert step name --- osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs b/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs index cedbb5025f..b34974dfc7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("set current to 3/4", () => timeSignature.Current.Value = TimeSignature.SimpleTriple); AddAssert("current is 3/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleTriple)); - AddAssert("numerator is 5", () => numeratorTextBox.Current.Value == "3"); + AddAssert("numerator is 3", () => numeratorTextBox.Current.Value == "3"); } [Test] From e4758c9dbbee726593078de2c111fd243b4db582 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 23 Jan 2022 10:13:32 +0300 Subject: [PATCH 064/587] Mark `LabelledTimeSignature` as public --- osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs b/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs index 52aece75ad..66bd341393 100644 --- a/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs +++ b/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Screens.Edit.Timing { - internal class LabelledTimeSignature : LabelledComponent + public class LabelledTimeSignature : LabelledComponent { public LabelledTimeSignature() : base(false) @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Edit.Timing protected override TimeSignatureBox CreateComponent() => new TimeSignatureBox(); - internal class TimeSignatureBox : CompositeDrawable, IHasCurrentValue + public class TimeSignatureBox : CompositeDrawable, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(TimeSignature.SimpleQuadruple); From a5493ce0d1f70b6db0d9ce846ed14c42f360946e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 17:51:32 +0900 Subject: [PATCH 065/587] Fix incorrect nesting of statements causing completely broken logic --- osu.Game/Screens/Menu/IntroScreen.cs | 54 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 10be90a5f0..e66ecc74e1 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -99,51 +99,51 @@ namespace osu.Game.Screens.Menu { realmContextFactory.Run(realm => { - var sets = realm.All().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection(); + var usableBeatmapSets = realm.All().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection(); - int setCount = sets.Count; + int setCount = usableBeatmapSets.Count; if (setCount > 0) { - var found = sets[RNG.Next(0, setCount - 1)].Beatmaps.FirstOrDefault(); + var found = usableBeatmapSets[RNG.Next(0, setCount - 1)].Beatmaps.FirstOrDefault(); if (found != null) initialBeatmap = beatmaps.GetWorkingBeatmap(found); } }); + } - // we generally want a song to be playing on startup, so use the intro music even if a user has specified not to if no other track is available. - if (initialBeatmap == null) + // we generally want a song to be playing on startup, so use the intro music even if a user has specified not to if no other track is available. + if (initialBeatmap == null) + { + if (!loadThemedIntro()) { - if (!loadThemedIntro()) - { - // 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)).GetResultSafely(); + // 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)).GetResultSafely(); - import?.PerformWrite(b => b.Protected = true); + import?.PerformWrite(b => b.Protected = true); - loadThemedIntro(); - } + loadThemedIntro(); } + } - bool loadThemedIntro() + bool loadThemedIntro() + { + var setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash); + + if (setInfo == null) + return false; + + setInfo.PerformRead(s => { - var setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash); + if (s.Beatmaps.Count == 0) + return; - if (setInfo == null) - return false; + initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps.First()); + }); - setInfo.PerformRead(s => - { - if (s.Beatmaps.Count == 0) - return; - - initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps.First()); - }); - - return UsingThemedIntro = initialBeatmap != null; - } + return UsingThemedIntro = initialBeatmap != null; } } From 70a120ea8a5e7694c068624b6495fa857f95a4e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 18:00:24 +0900 Subject: [PATCH 066/587] Add missing lock coverage when using `subscriptionActions` dictionary --- osu.Game/Database/RealmContextFactory.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 6fb9a2996b..606871337c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -249,11 +249,13 @@ namespace osu.Game.Database return new InvokeOnDisposal(() => { - // TODO: this likely needs to be run on the update thread. - if (subscriptionActions.TryGetValue(action, out var unsubscriptionAction)) + lock (contextLock) { - unsubscriptionAction?.Dispose(); - subscriptionActions.Remove(action); + if (subscriptionActions.TryGetValue(action, out var unsubscriptionAction)) + { + unsubscriptionAction?.Dispose(); + subscriptionActions.Remove(action); + } } }); } From bd0eda7e908e7366198e0007d6db9434faded441 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 18:01:39 +0900 Subject: [PATCH 067/587] Use method instead of property for realm query retrieval --- .../Bindings/DatabasedKeyBindingContainer.cs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 5a9797ffce..ac33c64391 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -46,19 +46,16 @@ namespace osu.Game.Input.Bindings throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided."); } - private IQueryable realmKeyBindings + private IQueryable queryRealmKeyBindings() { - get - { - string rulesetName = ruleset?.ShortName; - return realmFactory.Context.All() - .Where(b => b.RulesetName == rulesetName && b.Variant == variant); - } + string rulesetName = ruleset?.ShortName; + return realmFactory.Context.All() + .Where(b => b.RulesetName == rulesetName && b.Variant == variant); } protected override void LoadComplete() { - realmSubscription = realmFactory.Register(realm => realmKeyBindings + realmSubscription = realmFactory.Register(realm => queryRealmKeyBindings() .QueryAsyncWithNotifications((sender, changes, error) => { // The first fire of this is a bit redundant as this is being called in base.LoadComplete, @@ -80,11 +77,11 @@ namespace osu.Game.Input.Bindings { var defaults = DefaultKeyBindings.ToList(); - List newBindings = realmKeyBindings.Detach() - // this ordering is important to ensure that we read entries from the database in the order - // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise - // have been eaten by the music controller due to query order. - .OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList(); + List newBindings = queryRealmKeyBindings().Detach() + // this ordering is important to ensure that we read entries from the database in the order + // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise + // have been eaten by the music controller due to query order. + .OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList(); // In the case no bindings were found in the database, presume this usage is for a non-databased ruleset. // This actually should never be required and can be removed if it is ever deemed to cause a problem. From f39ff1eacb3b5179f70aa47f0a892d23e2651e7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 18:13:28 +0900 Subject: [PATCH 068/587] Add unregistration on blocking This is the first part of the requirement of sending a `ChangeSet` event to ensure correct state during blocking time --- osu.Game/Database/RealmContextFactory.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 606871337c..3e0f278e30 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -247,6 +247,8 @@ namespace osu.Game.Database registerSubscription(action); + // This token is returned to the consumer only. + // It will cause the registration to be permanently removed. return new InvokeOnDisposal(() => { lock (contextLock) @@ -269,12 +271,27 @@ namespace osu.Game.Database lock (contextLock) { + Debug.Assert(!customSubscriptionActions.TryGetValue(action, out var found) || found == null); + current_thread_subscriptions_allowed.Value = true; subscriptionActions[action] = action(realm); current_thread_subscriptions_allowed.Value = false; } } + /// + /// Unregister all subscriptions when the realm context is to be recycled. + /// Subscriptions will still remain and will be re-subscribed when the realm context returns. + /// + private void unregisterAllSubscriptions() + { + foreach (var action in subscriptionActions) + { + action.Value?.Dispose(); + subscriptionActions[action.Key] = null; + } + } + private Realm createContext() { if (isDisposed) @@ -519,6 +536,7 @@ namespace osu.Game.Database throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); syncContext = SynchronizationContext.Current; + unregisterAllSubscriptions(); Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); From 61cef42be977be4fe3cd612122de1f52556e03d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 19:42:26 +0900 Subject: [PATCH 069/587] Proof of concept realm subscriptions via `Register` --- osu.Game/Database/EmptyRealmSet.cs | 74 ++++++++++++++++++++ osu.Game/Database/RealmContextFactory.cs | 33 +++++++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 21 +++--- 4 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Database/EmptyRealmSet.cs diff --git a/osu.Game/Database/EmptyRealmSet.cs b/osu.Game/Database/EmptyRealmSet.cs new file mode 100644 index 0000000000..2fecfcbe07 --- /dev/null +++ b/osu.Game/Database/EmptyRealmSet.cs @@ -0,0 +1,74 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using Realms; +using Realms.Schema; + +#nullable enable + +namespace osu.Game.Database +{ + public class EmptyRealmSet : IRealmCollection + { + private static List emptySet => new List(); + + public IEnumerator GetEnumerator() + { + return emptySet.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)emptySet).GetEnumerator(); + } + + public int Count => emptySet.Count; + + public T this[int index] => emptySet[index]; + + public event NotifyCollectionChangedEventHandler? CollectionChanged + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + public event PropertyChangedEventHandler? PropertyChanged + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + public int IndexOf(object item) + { + return emptySet.IndexOf((T)item); + } + + public bool Contains(object item) + { + return emptySet.Contains((T)item); + } + + public IRealmCollection Freeze() + { + throw new NotImplementedException(); + } + + public IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback) + { + throw new NotImplementedException(); + } + + public bool IsValid => throw new NotImplementedException(); + + public Realm Realm => throw new NotImplementedException(); + + public ObjectSchema ObjectSchema => throw new NotImplementedException(); + + public bool IsFrozen => throw new NotImplementedException(); + } +} diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 3e0f278e30..32f7ac99c1 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -84,7 +84,7 @@ namespace osu.Game.Database Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}"); // Resubscribe any subscriptions - foreach (var action in subscriptionActions.Keys) + foreach (var action in customSubscriptionActions.Keys) registerSubscription(action); } @@ -233,7 +233,22 @@ namespace osu.Game.Database } } - private readonly Dictionary, IDisposable?> subscriptionActions = new Dictionary, IDisposable?>(); + private readonly Dictionary, IDisposable?> customSubscriptionActions = new Dictionary, IDisposable?>(); + + private readonly Dictionary, Action> realmSubscriptionsResetMap = new Dictionary, Action>(); + + public IDisposable Register(Func> query, NotificationCallbackDelegate onChanged) + where T : RealmObjectBase + { + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException(@$"{nameof(Register)} must be called from the update thread."); + + realmSubscriptionsResetMap.Add(action, () => onChanged(new EmptyRealmSet(), null, null)); + + return Register(action); + + IDisposable? action(Realm realm) => query(realm).QueryAsyncWithNotifications(onChanged); + } /// /// Run work on realm that will be run every time the update thread realm context gets recycled. @@ -253,10 +268,11 @@ namespace osu.Game.Database { lock (contextLock) { - if (subscriptionActions.TryGetValue(action, out var unsubscriptionAction)) + if (customSubscriptionActions.TryGetValue(action, out var unsubscriptionAction)) { unsubscriptionAction?.Dispose(); - subscriptionActions.Remove(action); + customSubscriptionActions.Remove(action); + realmSubscriptionsResetMap.Remove(action); } } }); @@ -274,7 +290,7 @@ namespace osu.Game.Database Debug.Assert(!customSubscriptionActions.TryGetValue(action, out var found) || found == null); current_thread_subscriptions_allowed.Value = true; - subscriptionActions[action] = action(realm); + customSubscriptionActions[action] = action(realm); current_thread_subscriptions_allowed.Value = false; } } @@ -285,10 +301,13 @@ namespace osu.Game.Database /// private void unregisterAllSubscriptions() { - foreach (var action in subscriptionActions) + foreach (var action in realmSubscriptionsResetMap.Values) + action(); + + foreach (var action in customSubscriptionActions) { action.Value?.Dispose(); - subscriptionActions[action.Key] = null; + customSubscriptionActions[action.Key] = null; } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 6f3f467170..10ba23985e 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -190,7 +190,7 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); - subscriptionSets = realmFactory.Register(realm => getBeatmapSets(realm).QueryAsyncWithNotifications(beatmapSetsChanged)); + subscriptionSets = realmFactory.Register(getBeatmapSets, beatmapSetsChanged); subscriptionBeatmaps = realmFactory.Register(realm => realm.All().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged)); // Can't use main subscriptions because we can't lookup deleted indices. @@ -274,7 +274,7 @@ namespace osu.Game.Screens.Select } } - private IRealmCollection getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection(); + private IQueryable getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected); public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => removeBeatmapSet(beatmapSet.ID); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 9fa5bb8562..904648f727 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -17,6 +17,7 @@ using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Scoring; +using Realms; namespace osu.Game.Screens.Spectate { @@ -79,23 +80,21 @@ namespace osu.Game.Screens.Spectate playingUserStates.BindTo(spectatorClient.PlayingUserStates); playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); - realmSubscription = realmContextFactory.Register(realm => - realm.All() - .Where(s => !s.DeletePending) - .QueryAsyncWithNotifications((items, changes, ___) => - { - if (changes?.InsertedIndices == null) - return; - - foreach (int c in changes.InsertedIndices) - beatmapUpdated(items[c]); - })); + realmSubscription = realmContextFactory.Register( + realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); foreach ((int id, var _) in userMap) spectatorClient.WatchUser(id); })); } + private void beatmapsChanged(IRealmCollection items, ChangeSet changes, Exception ___) + { + if (changes?.InsertedIndices == null) return; + + foreach (int c in changes.InsertedIndices) beatmapUpdated(items[c]); + } + private void beatmapUpdated(BeatmapSetInfo beatmapSet) { foreach ((int userId, _) in userMap) From e9e3e024a19d4867673152efa06f89842937d032 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 19:50:29 +0900 Subject: [PATCH 070/587] Update all usages of `QueryAsyncWithNotifications` to use new `Register` pathway --- .../Bindings/DatabasedKeyBindingContainer.cs | 13 +++++----- osu.Game/Online/BeatmapDownloadTracker.cs | 4 ++-- .../OnlinePlayBeatmapAvailabilityTracker.cs | 4 ++-- osu.Game/Online/ScoreDownloadTracker.cs | 4 ++-- osu.Game/Overlays/MusicController.cs | 2 +- .../Overlays/Settings/Sections/SkinSection.cs | 19 +++++++-------- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 ++--- .../Screens/Select/Carousel/TopLocalRank.cs | 24 +++++++++---------- .../Select/Leaderboards/BeatmapLeaderboard.cs | 14 +++++------ 9 files changed, 44 insertions(+), 46 deletions(-) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index ac33c64391..4ad5693867 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -55,13 +55,12 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { - realmSubscription = realmFactory.Register(realm => queryRealmKeyBindings() - .QueryAsyncWithNotifications((sender, changes, error) => - { - // The first fire of this is a bit redundant as this is being called in base.LoadComplete, - // but this is safest in case the subscription is restored after a context recycle. - ReloadMappings(); - })); + realmSubscription = realmFactory.Register(realm => queryRealmKeyBindings(), (sender, changes, error) => + { + // The first fire of this is a bit redundant as this is being called in base.LoadComplete, + // but this is safest in case the subscription is restored after a context recycle. + ReloadMappings(); + }); base.LoadComplete(); } diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index d50ab5a347..70599a167b 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realmContextFactory.Register(realm => realm.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realmContextFactory.Register(realm => realm.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); @@ -54,7 +54,7 @@ namespace osu.Game.Online attachDownload(Downloader.GetExistingDownload(beatmapSetInfo)); }); } - })); + }); } private void downloadBegan(ArchiveDownloadRequest request) => Schedule(() => diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index fdcf2b39c6..8dd28f5417 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -78,13 +78,13 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realmContextFactory.Register(realm => filteredBeatmaps().QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realmContextFactory.Register(realm => filteredBeatmaps(), (items, changes, ___) => { if (changes == null) return; Scheduler.AddOnce(updateAvailability); - })); + }); }, true); } diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index de1d6fd94a..72e95bd6df 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; - realmSubscription = realmContextFactory.Register(realm => realm.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realmContextFactory.Register(realm => realm.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending), (items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); @@ -59,7 +59,7 @@ namespace osu.Game.Online attachDownload(Downloader.GetExistingDownload(scoreInfo)); }); } - })); + }); } private void downloadBegan(ArchiveDownloadRequest request) => Schedule(() => diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 01a2c9d354..24d907285a 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -93,7 +93,7 @@ namespace osu.Game.Overlays foreach (var s in availableBeatmaps) beatmapSets.Add(s.Detach()); - beatmapSubscription = realmFactory.Register(realm => availableBeatmaps.QueryAsyncWithNotifications(beatmapsChanged)); + beatmapSubscription = realmFactory.Register(realm => availableBeatmaps, beatmapsChanged); } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 11a7275168..c767edec71 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections private IDisposable realmSubscription; - private IQueryable realmSkins => + private IQueryable queryRealmSkins() => realmFactory.Context.All() .Where(s => !s.DeletePending) .OrderByDescending(s => s.Protected) // protected skins should be at the top. @@ -83,13 +83,12 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Current = dropdownBindable; - realmSubscription = realmFactory.Register(realm => realmSkins - .QueryAsyncWithNotifications((sender, changes, error) => - { - // The first fire of this is a bit redundant due to the call below, - // but this is safest in case the subscription is restored after a context recycle. - updateItems(); - })); + realmSubscription = realmFactory.Register(realm => queryRealmSkins(), (sender, changes, error) => + { + // The first fire of this is a bit redundant due to the call below, + // but this is safest in case the subscription is restored after a context recycle. + updateItems(); + }); updateItems(); @@ -129,9 +128,9 @@ namespace osu.Game.Overlays.Settings.Sections private void updateItems() { - int protectedCount = realmSkins.Count(s => s.Protected); + int protectedCount = queryRealmSkins().Count(s => s.Protected); - skinItems = realmSkins.ToLive(realmFactory); + skinItems = queryRealmSkins().ToLive(realmFactory); skinItems.Insert(protectedCount, random_skin_info); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 10ba23985e..889a6f5b79 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -191,12 +191,12 @@ namespace osu.Game.Screens.Select base.LoadComplete(); subscriptionSets = realmFactory.Register(getBeatmapSets, beatmapSetsChanged); - subscriptionBeatmaps = realmFactory.Register(realm => realm.All().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged)); + subscriptionBeatmaps = realmFactory.Register(realm => realm.All().Where(b => !b.Hidden), beatmapsChanged); // Can't use main subscriptions because we can't lookup deleted indices. // https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595. - subscriptionDeletedSets = realmFactory.Register(realm => realm.All().Where(s => s.DeletePending && !s.Protected).QueryAsyncWithNotifications(deletedBeatmapSetsChanged)); - subscriptionHiddenBeatmaps = realmFactory.Register(realm => realm.All().Where(b => b.Hidden).QueryAsyncWithNotifications(beatmapsChanged)); + subscriptionDeletedSets = realmFactory.Register(realm => realm.All().Where(s => s.DeletePending && !s.Protected), deletedBeatmapSetsChanged); + subscriptionHiddenBeatmaps = realmFactory.Register(realm => realm.All().Where(b => b.Hidden), beatmapsChanged); } private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index ee3930364b..fc2188e597 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -49,18 +49,18 @@ namespace osu.Game.Screens.Select.Carousel { scoreSubscription?.Dispose(); scoreSubscription = realmFactory.Register(realm => - realm.All() - .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" - + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" - + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName) - .OrderByDescending(s => s.TotalScore) - .QueryAsyncWithNotifications((items, changes, ___) => - { - Rank = items.FirstOrDefault()?.Rank; - // Required since presence is changed via IsPresent override - Invalidate(Invalidation.Presence); - })); + realm.All() + .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" + + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" + + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName) + .OrderByDescending(s => s.TotalScore), + (items, changes, ___) => + { + Rank = items.FirstOrDefault()?.Rank; + // Required since presence is changed via IsPresent override + Invalidate(Invalidation.Presence); + }); }, true); } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 954c2a6413..463f878e2a 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -114,13 +114,13 @@ namespace osu.Game.Screens.Select.Leaderboards return; scoreSubscription = realmFactory.Register(realm => - realm.All() - .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID) - .QueryAsyncWithNotifications((_, changes, ___) => - { - if (!IsOnlineScope) - RefreshScores(); - })); + realm.All() + .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID), + (_, changes, ___) => + { + if (!IsOnlineScope) + RefreshScores(); + }); } protected override void Reset() From db8639435538108982acffad2d9e8da710dd71e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 20:27:35 +0900 Subject: [PATCH 071/587] Fix `TestResources` returning a test `BeatmapSetInfo` that can't be laoded directly into realm --- osu.Game.Tests/Resources/TestResources.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index d2cab09ac9..81b624f908 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -80,7 +80,10 @@ namespace osu.Game.Tests.Resources public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null) { int j = 0; - RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length] ?? new OsuRuleset().RulesetInfo; + + rulesets ??= new[] { new OsuRuleset().RulesetInfo }; + + RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length]; int setId = Interlocked.Increment(ref importId); From 0709a2ac9be3376193a4aeb266b497683b1bde7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 20:28:13 +0900 Subject: [PATCH 072/587] Add test coverage of realm subscription scenarios --- .../RealmSubscriptionRegistrationTests.cs | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs new file mode 100644 index 0000000000..16e9f31c7b --- /dev/null +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -0,0 +1,138 @@ +// 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; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Tests.Resources; +using Realms; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public class RealmSubscriptionRegistrationTests : RealmTest + { + [Test] + public void TestSubscriptionWithContextLoss() + { + IEnumerable? resolvedItems = null; + ChangeSet? lastChanges = null; + + RunTestWithRealm((realmFactory, _) => + { + realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); + + var registration = realmFactory.Register(realm => realm.All(), onChanged); + + testEventsArriving(true); + + // All normal until here. + // Now let's yank the main realm context. + resolvedItems = null; + lastChanges = null; + + using (realmFactory.BlockAllOperations()) + Assert.That(resolvedItems, Is.Empty); + + realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); + + testEventsArriving(true); + + // Now let's try unsubscribing. + resolvedItems = null; + lastChanges = null; + + registration.Dispose(); + + realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); + + testEventsArriving(false); + + // And make sure even after another context loss we don't get firings. + using (realmFactory.BlockAllOperations()) + Assert.That(resolvedItems, Is.Null); + + realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); + + testEventsArriving(false); + + void testEventsArriving(bool shouldArrive) + { + realmFactory.Run(realm => realm.Refresh()); + + if (shouldArrive) + Assert.That(resolvedItems, Has.One.Items); + else + Assert.That(resolvedItems, Is.Null); + + realmFactory.Write(realm => + { + realm.RemoveAll(); + realm.RemoveAll(); + }); + + realmFactory.Run(realm => realm.Refresh()); + + if (shouldArrive) + Assert.That(lastChanges?.DeletedIndices, Has.One.Items); + else + Assert.That(lastChanges, Is.Null); + } + }); + + void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + { + if (changes == null) + resolvedItems = sender; + + lastChanges = changes; + } + } + + [Test] + public void TestCustomRegisterWithContextLoss() + { + RunTestWithRealm((realmFactory, _) => + { + BeatmapSetInfo? beatmapSetInfo = null; + + realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); + + var subscription = realmFactory.Register(realm => + { + beatmapSetInfo = realm.All().First(); + + return new InvokeOnDisposal(() => beatmapSetInfo = null); + }); + + Assert.That(beatmapSetInfo, Is.Not.Null); + + using (realmFactory.BlockAllOperations()) + { + // custom disposal action fired when context lost. + Assert.That(beatmapSetInfo, Is.Null); + } + + // re-registration after context restore. + realmFactory.Run(realm => realm.Refresh()); + Assert.That(beatmapSetInfo, Is.Not.Null); + + subscription.Dispose(); + + Assert.That(beatmapSetInfo, Is.Null); + + using (realmFactory.BlockAllOperations()) + Assert.That(beatmapSetInfo, Is.Null); + + realmFactory.Run(realm => realm.Refresh()); + Assert.That(beatmapSetInfo, Is.Null); + }); + } + } +} From 5e7993c35afaae58a3dec41dddd0463b4e07a53a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 20:38:34 +0900 Subject: [PATCH 073/587] Post disposal to synchronisation context --- osu.Game/Database/RealmContextFactory.cs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 32f7ac99c1..26943c1951 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -260,19 +260,29 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException(@$"{nameof(Register)} must be called from the update thread."); + var syncContext = SynchronizationContext.Current; + registerSubscription(action); // This token is returned to the consumer only. // It will cause the registration to be permanently removed. return new InvokeOnDisposal(() => { - lock (contextLock) + if (ThreadSafety.IsUpdateThread) + unsubscribe(); + else + syncContext.Post(_ => unsubscribe(), null); + + void unsubscribe() { - if (customSubscriptionActions.TryGetValue(action, out var unsubscriptionAction)) + lock (contextLock) { - unsubscriptionAction?.Dispose(); - customSubscriptionActions.Remove(action); - realmSubscriptionsResetMap.Remove(action); + if (customSubscriptionActions.TryGetValue(action, out var unsubscriptionAction)) + { + unsubscriptionAction?.Dispose(); + customSubscriptionActions.Remove(action); + realmSubscriptionsResetMap.Remove(action); + } } } }); From 249f0f9697f828649c1734eeec281e0b0876ba77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 23:15:39 +0900 Subject: [PATCH 074/587] Add more lengthy comment explaining cyclic avoidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Database/RealmContextFactory.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 26943c1951..c8fa298f91 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -292,7 +292,9 @@ namespace osu.Game.Database { Debug.Assert(ThreadSafety.IsUpdateThread); - // Get context outside of flag update to ensure beyond doubt this can't be cyclic. + // Retrieve context outside of flag update to ensure that the context is constructed, + // as attempting to access it inside the subscription if it's not constructed would lead to + // cyclic invocations of the subscription callback. var realm = Context; lock (contextLock) From deb167086212faa8d6c51fe911bcd54c1cc85c73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 23:17:33 +0900 Subject: [PATCH 075/587] Use `Array.Empty` instead of constructed list --- osu.Game/Database/EmptyRealmSet.cs | 42 +++++------------------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/osu.Game/Database/EmptyRealmSet.cs b/osu.Game/Database/EmptyRealmSet.cs index 2fecfcbe07..b7f27ba035 100644 --- a/osu.Game/Database/EmptyRealmSet.cs +++ b/osu.Game/Database/EmptyRealmSet.cs @@ -15,21 +15,14 @@ namespace osu.Game.Database { public class EmptyRealmSet : IRealmCollection { - private static List emptySet => new List(); - - public IEnumerator GetEnumerator() - { - return emptySet.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable)emptySet).GetEnumerator(); - } + private IList emptySet => Array.Empty(); + public IEnumerator GetEnumerator() => emptySet.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator(); public int Count => emptySet.Count; - public T this[int index] => emptySet[index]; + public int IndexOf(object item) => emptySet.IndexOf((T)item); + public bool Contains(object item) => emptySet.Contains((T)item); public event NotifyCollectionChangedEventHandler? CollectionChanged { @@ -43,32 +36,11 @@ namespace osu.Game.Database remove => throw new NotImplementedException(); } - public int IndexOf(object item) - { - return emptySet.IndexOf((T)item); - } - - public bool Contains(object item) - { - return emptySet.Contains((T)item); - } - - public IRealmCollection Freeze() - { - throw new NotImplementedException(); - } - - public IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback) - { - throw new NotImplementedException(); - } - + public IRealmCollection Freeze() => throw new NotImplementedException(); + public IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback) => throw new NotImplementedException(); public bool IsValid => throw new NotImplementedException(); - public Realm Realm => throw new NotImplementedException(); - public ObjectSchema ObjectSchema => throw new NotImplementedException(); - public bool IsFrozen => throw new NotImplementedException(); } } From 351c766ea1f211b8eebe8ff58cb7d5fd0e813739 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jan 2022 23:20:03 +0900 Subject: [PATCH 076/587] Fix one remaining instance of realm query as property --- osu.Game/Overlays/MusicController.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 24d907285a..8caa69e78c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -80,9 +80,10 @@ namespace osu.Game.Overlays mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } - private IQueryable availableBeatmaps => realmFactory.Context - .All() - .Where(s => !s.DeletePending); + private IQueryable queryRealmBeatmapSets() => + realmFactory.Context + .All() + .Where(s => !s.DeletePending); protected override void LoadComplete() { @@ -90,10 +91,10 @@ namespace osu.Game.Overlays // ensure we're ready before completing async load. // probably not a good way of handling this (as there is a period we aren't watching for changes until the realm subscription finishes up. - foreach (var s in availableBeatmaps) + foreach (var s in queryRealmBeatmapSets()) beatmapSets.Add(s.Detach()); - beatmapSubscription = realmFactory.Register(realm => availableBeatmaps, beatmapsChanged); + beatmapSubscription = realmFactory.Register(realm => queryRealmBeatmapSets(), beatmapsChanged); } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) From 4e5a1f27a8c35f462385dfecdeffe9e11d8c14b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 15:14:53 +0100 Subject: [PATCH 077/587] Initialise `Simple{Triple,Quadruple}` only once ever rather than create every time --- osu.Game/Beatmaps/Timing/TimeSignature.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Timing/TimeSignature.cs b/osu.Game/Beatmaps/Timing/TimeSignature.cs index 5bfeea5e9b..eebbcc34cd 100644 --- a/osu.Game/Beatmaps/Timing/TimeSignature.cs +++ b/osu.Game/Beatmaps/Timing/TimeSignature.cs @@ -27,8 +27,8 @@ namespace osu.Game.Beatmaps.Timing Numerator = numerator; } - public static TimeSignature SimpleTriple => new TimeSignature(3); - public static TimeSignature SimpleQuadruple => new TimeSignature(4); + public static TimeSignature SimpleTriple { get; } = new TimeSignature(3); + public static TimeSignature SimpleQuadruple { get; } = new TimeSignature(4); public override string ToString() => $"{Numerator}/4"; From bd748686fad63ccb40f13ae9498a53f2c3f47570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 15:20:51 +0100 Subject: [PATCH 078/587] Adjust spacing of time signature numerator input box --- osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs b/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs index 66bd341393..51b58bd3dc 100644 --- a/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs +++ b/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs @@ -57,8 +57,12 @@ namespace osu.Game.Screens.Edit.Timing { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Margin = new MarginPadding(10), - Text = "/4", + Margin = new MarginPadding + { + Left = 5, + Right = CONTENT_PADDING_HORIZONTAL + }, + Text = "/ 4", Font = OsuFont.Default.With(size: 20) } } From e236f5d604ee30d92fffccac17702ea4d341abc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 20:28:19 +0100 Subject: [PATCH 079/587] Add failing test coverage for correct beatmap filename generation on save --- osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index f89be0adf3..bf3b46c6f7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -46,6 +46,7 @@ namespace osu.Game.Tests.Visual.Editing editorBeatmap.BeatmapInfo.Metadata.Artist = "artist"; editorBeatmap.BeatmapInfo.Metadata.Title = "title"; }); + AddStep("Set author", () => editorBeatmap.BeatmapInfo.Metadata.Author.Username = "author"); AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty"); AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); @@ -64,6 +65,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Save", () => InputManager.Keys(PlatformAction.Save)); checkMutations(); + AddAssert("Beatmap has correct .osu file path", () => editorBeatmap.BeatmapInfo.Path == "artist - title (author) [difficulty].osu"); AddStep("Exit", () => InputManager.Key(Key.Escape)); @@ -88,6 +90,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7); AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title"); + AddAssert("Beatmap has correct author", () => editorBeatmap.BeatmapInfo.Metadata.Author.Username == "author"); AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty"); } } From 838a9f69ed078e6e954ca393e1aa5d6fee9ac4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 19:41:41 +0100 Subject: [PATCH 080/587] Fix saved beatmap filename depending on `ToString()` implementation --- osu.Game/Beatmaps/BeatmapModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 44d6af5b73..ead86c1059 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -88,7 +88,7 @@ namespace osu.Game.Beatmaps private static string getFilename(BeatmapInfo beatmapInfo) { var metadata = beatmapInfo.Metadata; - return $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename(); + return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename(); } /// From ed84ae0ac0d8f57f66ee7e2360b3259e6a786e2c Mon Sep 17 00:00:00 2001 From: mk-56 Date: Mon, 24 Jan 2022 00:42:43 +0100 Subject: [PATCH 081/587] Adjust values to Bdach's refined taste --- .../Mods/CatchModFlashlight.cs | 22 ++++++++++++------- .../Mods/ManiaModFlashlight.cs | 20 +++++++++++------ .../Mods/OsuModFlashlight.cs | 22 ++++++++++++------- .../Mods/TaikoModFlashlight.cs | 22 ++++++++++++------- osu.Game/Rulesets/Mods/ModFlashlight.cs | 13 +++++++---- 5 files changed, 64 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index f75772b04e..e8f1ebdd10 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -26,14 +26,20 @@ namespace osu.Game.Rulesets.Catch.Mods [SettingSource("Initial radius", "Initial radius of the flashlight area.")] public override BindableNumber InitialRadius { get; } = new BindableNumber { - MinValue = 150f, - MaxValue = 600f, - Default = 350f, - Value = 350f, - Precision = 5f + MinValue = 0.4f, + MaxValue = 1.7f, + Default = 1f, + Value = 1f, + Precision = 0.1f }; - public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value); + protected override BindableNumber ModeMultiplier { get; } = new BindableNumber + { + Default = 350, + Value = 350, + }; + + public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, ModeMultiplier.Value); private CatchPlayfield playfield; @@ -47,8 +53,8 @@ namespace osu.Game.Rulesets.Catch.Mods { private readonly CatchPlayfield playfield; - public CatchFlashlight(CatchPlayfield playfield, bool isRadiusBasedOnCombo, float initialRadius) - : base(isRadiusBasedOnCombo, initialRadius) + public CatchFlashlight(CatchPlayfield playfield, bool isRadiusBasedOnCombo, float initialRadius, float modeMultiplier) + : base(isRadiusBasedOnCombo, initialRadius, modeMultiplier) { this.playfield = playfield; FlashlightSize = new Vector2(0, GetRadiusFor(0)); diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index a6a3c3be73..9ca1a72584 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -28,20 +28,26 @@ namespace osu.Game.Rulesets.Mania.Mods public override BindableNumber InitialRadius { get; } = new BindableNumber { MinValue = 0f, - MaxValue = 230f, - Default = 50f, - Value = 50f, - Precision = 5f + MaxValue = 4.5f, + Default = 1f, + Value = 1f, + Precision = 0.1f }; - public override Flashlight CreateFlashlight() => new ManiaFlashlight(ChangeRadius.Value, InitialRadius.Value); + protected override BindableNumber ModeMultiplier { get; } = new BindableNumber + { + Default = 50, + Value = 50, + }; + + public override Flashlight CreateFlashlight() => new ManiaFlashlight(ChangeRadius.Value, InitialRadius.Value, ModeMultiplier.Value); private class ManiaFlashlight : Flashlight { private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); - public ManiaFlashlight(bool isRadiusBasedOnCombo, float initialRadius) - : base(isRadiusBasedOnCombo, initialRadius) + public ManiaFlashlight(bool isRadiusBasedOnCombo, float initialRadius, float modeMultiplier) + : base(isRadiusBasedOnCombo, initialRadius, modeMultiplier) { FlashlightSize = new Vector2(DrawWidth, GetRadiusFor(0)); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index e2a6d0f0dc..c6cf5ce4b5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -40,16 +40,22 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Initial radius", "Initial radius of the flashlight area.")] public override BindableNumber InitialRadius { get; } = new BindableNumber { - MinValue = 90f, - MaxValue = 360f, - Default = 180f, - Value = 180f, - Precision = 5f + MinValue = 0.5f, + MaxValue = 2f, + Default = 1f, + Value = 1f, + Precision = 0.1f + }; + + protected override BindableNumber ModeMultiplier { get; } = new BindableNumber + { + Default = 180, + Value = 180, }; private OsuFlashlight flashlight; - public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(ChangeRadius.Value, InitialRadius.Value, FollowDelay.Value); + public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(ChangeRadius.Value, InitialRadius.Value, FollowDelay.Value, ModeMultiplier.Value); public void ApplyToDrawableHitObject(DrawableHitObject drawable) { @@ -64,8 +70,8 @@ namespace osu.Game.Rulesets.Osu.Mods //public float InitialRadius { private get; set; } public bool ChangeRadius { private get; set; } - public OsuFlashlight(bool isRadiusBasedOnCombo, float initialRadius, double followDelay) - : base(isRadiusBasedOnCombo, initialRadius) + public OsuFlashlight(bool isRadiusBasedOnCombo, float initialRadius, double followDelay, float modeMultiplier) + : base(isRadiusBasedOnCombo, initialRadius, modeMultiplier) { FollowDelay = followDelay; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index 71c9d777ec..f235698b55 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -27,14 +27,20 @@ namespace osu.Game.Rulesets.Taiko.Mods [SettingSource("Initial radius", "Initial radius of the flashlight area.")] public override BindableNumber InitialRadius { get; } = new BindableNumber { - MinValue = 0f, - MaxValue = 400f, - Default = 250f, - Value = 250f, - Precision = 5f + MinValue = 0, + MaxValue = 1.66f, + Default = 1f, + Value = 1f, + Precision = 0.1f }; - public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value); + protected override BindableNumber ModeMultiplier { get; } = new BindableNumber + { + Default = 250, + Value = 250, + }; + + public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, ModeMultiplier.Value); private TaikoPlayfield playfield; @@ -49,8 +55,8 @@ namespace osu.Game.Rulesets.Taiko.Mods private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); private readonly TaikoPlayfield taikoPlayfield; - public TaikoFlashlight(TaikoPlayfield taikoPlayfield, bool isRadiusBasedOnCombo, float initialRadius) - : base(isRadiusBasedOnCombo, initialRadius) + public TaikoFlashlight(TaikoPlayfield taikoPlayfield, bool isRadiusBasedOnCombo, float initialRadius, float modeMultiplier) + : base(isRadiusBasedOnCombo, initialRadius, modeMultiplier) { this.taikoPlayfield = taikoPlayfield; FlashlightSize = getSizeFor(0); diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 51006d96e8..d16f310582 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Initial radius", "Initial radius of the flashlight area.")] public abstract BindableNumber InitialRadius { get; } + + protected abstract BindableNumber ModeMultiplier { get; } } public abstract class ModFlashlight : ModFlashlight, IApplicableToDrawableRuleset, IApplicableToScoreProcessor @@ -100,10 +102,13 @@ namespace osu.Game.Rulesets.Mods public readonly float InitialRadius; - protected Flashlight(bool isRadiusBasedOnCombo, float initialRadius) + public readonly float ModeMultiplier; + + protected Flashlight(bool isRadiusBasedOnCombo, float initialRadius, float modeMultiplier) { IsRadiusBasedOnCombo = isRadiusBasedOnCombo; InitialRadius = initialRadius; + ModeMultiplier = modeMultiplier; } [BackgroundDependencyLoader] @@ -142,12 +147,12 @@ namespace osu.Game.Rulesets.Mods if (IsRadiusBasedOnCombo) { if (combo > 200) - return InitialRadius * 0.8f; + return InitialRadius * 0.8f * ModeMultiplier; else if (combo > 100) - return InitialRadius * 0.9f; + return InitialRadius * 0.9f * ModeMultiplier; } - return InitialRadius; + return InitialRadius * ModeMultiplier; } private Vector2 flashlightPosition; From 997c13f64367f07fcd81f0107408774d2f0fd241 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 13:36:51 +0900 Subject: [PATCH 082/587] Add locking over `realmSubscriptionsResetMap` for sanity --- osu.Game/Database/RealmContextFactory.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c8fa298f91..522a5fdfd9 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -243,9 +243,11 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException(@$"{nameof(Register)} must be called from the update thread."); - realmSubscriptionsResetMap.Add(action, () => onChanged(new EmptyRealmSet(), null, null)); - - return Register(action); + lock (contextLock) + { + realmSubscriptionsResetMap.Add(action, () => onChanged(new EmptyRealmSet(), null, null)); + return Register(action); + } IDisposable? action(Realm realm) => query(realm).QueryAsyncWithNotifications(onChanged); } From d7a9c5fd410e370f89e83ef1bd3475a418ffe4ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 14:36:48 +0900 Subject: [PATCH 083/587] Add settings buttons to allow temporarily blocking realm access --- .../Sections/DebugSettings/MemorySettings.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index 8d4fc5fc9f..f058c274e4 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -17,6 +19,9 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings [BackgroundDependencyLoader] private void load(GameHost host, RealmContextFactory realmFactory) { + SettingsButton blockAction; + SettingsButton unblockAction; + Children = new Drawable[] { new SettingsButton @@ -35,6 +40,43 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings } } }, + blockAction = new SettingsButton + { + Text = "Block realm", + }, + unblockAction = new SettingsButton + { + Text = "Unblock realm", + }, + }; + + blockAction.Action = () => + { + var blocking = realmFactory.BlockAllOperations(); + blockAction.Enabled.Value = false; + + // As a safety measure, unblock after 10 seconds. + // This is to handle the case where a dev may block, but then something on the update thread + // accesses realm and blocks for eternity. + Task.Factory.StartNew(() => + { + Thread.Sleep(10000); + unblock(); + }); + + unblockAction.Action = unblock; + + void unblock() + { + blocking?.Dispose(); + blocking = null; + + Scheduler.Add(() => + { + blockAction.Enabled.Value = true; + unblockAction.Action = null; + }); + } }; } } From 40aa8731900bd7f856e5f5b417aa85897bf79f0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 14:37:36 +0900 Subject: [PATCH 084/587] Rename register methods to better explain their purpose --- osu.Game.Tests/Database/GeneralUsageTests.cs | 2 +- osu.Game.Tests/Database/RealmLiveTests.cs | 2 +- .../RealmSubscriptionRegistrationTests.cs | 4 ++-- osu.Game/Database/RealmContextFactory.cs | 18 +++++++++--------- osu.Game/Database/RealmObjectExtensions.cs | 2 +- .../Bindings/DatabasedKeyBindingContainer.cs | 2 +- osu.Game/Online/BeatmapDownloadTracker.cs | 2 +- .../OnlinePlayBeatmapAvailabilityTracker.cs | 2 +- osu.Game/Online/ScoreDownloadTracker.cs | 2 +- osu.Game/Overlays/MusicController.cs | 2 +- .../Overlays/Settings/Sections/SkinSection.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 8 ++++---- .../Screens/Select/Carousel/TopLocalRank.cs | 2 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- 15 files changed, 27 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index c82c1b6e59..3c62153d9e 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Database { bool callbackRan = false; - realmFactory.Register(realm => + realmFactory.RegisterCustomSubscription(realm => { var subscription = realm.All().QueryAsyncWithNotifications((sender, changes, error) => { diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 0e6ad910d9..d53fcb9ac7 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -239,7 +239,7 @@ namespace osu.Game.Tests.Database { int changesTriggered = 0; - realmFactory.Register(outerRealm => + realmFactory.RegisterCustomSubscription(outerRealm => { outerRealm.All().QueryAsyncWithNotifications(gotChange); ILive? liveBeatmap = null; diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 16e9f31c7b..1799b95905 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Database { realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); - var registration = realmFactory.Register(realm => realm.All(), onChanged); + var registration = realmFactory.RegisterForNotifications(realm => realm.All(), onChanged); testEventsArriving(true); @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Database realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); - var subscription = realmFactory.Register(realm => + var subscription = realmFactory.RegisterCustomSubscription(realm => { beatmapSetInfo = realm.All().First(); diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 522a5fdfd9..697caf8cd3 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -63,6 +63,10 @@ namespace osu.Game.Database private readonly ThreadLocal currentThreadCanCreateContexts = new ThreadLocal(); + private readonly Dictionary, IDisposable?> customSubscriptionActions = new Dictionary, IDisposable?>(); + + private readonly Dictionary, Action> realmSubscriptionsResetMap = new Dictionary, Action>(); + private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)"); private readonly object contextLock = new object(); @@ -233,20 +237,16 @@ namespace osu.Game.Database } } - private readonly Dictionary, IDisposable?> customSubscriptionActions = new Dictionary, IDisposable?>(); - - private readonly Dictionary, Action> realmSubscriptionsResetMap = new Dictionary, Action>(); - - public IDisposable Register(Func> query, NotificationCallbackDelegate onChanged) + public IDisposable RegisterForNotifications(Func> query, NotificationCallbackDelegate onChanged) where T : RealmObjectBase { if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException(@$"{nameof(Register)} must be called from the update thread."); + throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); lock (contextLock) { realmSubscriptionsResetMap.Add(action, () => onChanged(new EmptyRealmSet(), null, null)); - return Register(action); + return RegisterCustomSubscription(action); } IDisposable? action(Realm realm) => query(realm).QueryAsyncWithNotifications(onChanged); @@ -257,10 +257,10 @@ namespace osu.Game.Database /// /// The work to run. Return value should be an from QueryAsyncWithNotifications, or an to clean up any bindings. /// An which should be disposed to unsubscribe any inner subscription. - public IDisposable Register(Func action) + public IDisposable RegisterCustomSubscription(Func action) { if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException(@$"{nameof(Register)} must be called from the update thread."); + throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); var syncContext = SynchronizationContext.Current; diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index c30e1699b9..d9026d165d 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -272,7 +272,7 @@ namespace osu.Game.Database where T : RealmObjectBase { if (!RealmContextFactory.CurrentThreadSubscriptionsAllowed) - throw new InvalidOperationException($"Make sure to call {nameof(RealmContextFactory)}.{nameof(RealmContextFactory.Register)}"); + throw new InvalidOperationException($"Make sure to call {nameof(RealmContextFactory)}.{nameof(RealmContextFactory.RegisterForNotifications)}"); return collection.SubscribeForNotifications(callback); } diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 4ad5693867..9cd9865441 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -55,7 +55,7 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { - realmSubscription = realmFactory.Register(realm => queryRealmKeyBindings(), (sender, changes, error) => + realmSubscription = realmFactory.RegisterForNotifications(realm => queryRealmKeyBindings(), (sender, changes, error) => { // The first fire of this is a bit redundant as this is being called in base.LoadComplete, // but this is safest in case the subscription is restored after a context recycle. diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 70599a167b..3f45afb6b2 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realmContextFactory.Register(realm => realm.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, changes, ___) => + realmSubscription = realmContextFactory.RegisterForNotifications(realm => realm.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 8dd28f5417..28c41bec33 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -78,7 +78,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realmContextFactory.Register(realm => filteredBeatmaps(), (items, changes, ___) => + realmSubscription = realmContextFactory.RegisterForNotifications(realm => filteredBeatmaps(), (items, changes, ___) => { if (changes == null) return; diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index 72e95bd6df..64ceee9fe6 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; - realmSubscription = realmContextFactory.Register(realm => realm.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending), (items, changes, ___) => + realmSubscription = realmContextFactory.RegisterForNotifications(realm => realm.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending), (items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 8caa69e78c..8df7ed9736 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -94,7 +94,7 @@ namespace osu.Game.Overlays foreach (var s in queryRealmBeatmapSets()) beatmapSets.Add(s.Detach()); - beatmapSubscription = realmFactory.Register(realm => queryRealmBeatmapSets(), beatmapsChanged); + beatmapSubscription = realmFactory.RegisterForNotifications(realm => queryRealmBeatmapSets(), beatmapsChanged); } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index c767edec71..6e5cd0da2d 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -83,7 +83,7 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Current = dropdownBindable; - realmSubscription = realmFactory.Register(realm => queryRealmSkins(), (sender, changes, error) => + realmSubscription = realmFactory.RegisterForNotifications(realm => queryRealmSkins(), (sender, changes, error) => { // The first fire of this is a bit redundant due to the call below, // but this is safest in case the subscription is restored after a context recycle. diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 889a6f5b79..5a6295fe74 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -190,13 +190,13 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); - subscriptionSets = realmFactory.Register(getBeatmapSets, beatmapSetsChanged); - subscriptionBeatmaps = realmFactory.Register(realm => realm.All().Where(b => !b.Hidden), beatmapsChanged); + subscriptionSets = realmFactory.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); + subscriptionBeatmaps = realmFactory.RegisterForNotifications(realm => realm.All().Where(b => !b.Hidden), beatmapsChanged); // Can't use main subscriptions because we can't lookup deleted indices. // https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595. - subscriptionDeletedSets = realmFactory.Register(realm => realm.All().Where(s => s.DeletePending && !s.Protected), deletedBeatmapSetsChanged); - subscriptionHiddenBeatmaps = realmFactory.Register(realm => realm.All().Where(b => b.Hidden), beatmapsChanged); + subscriptionDeletedSets = realmFactory.RegisterForNotifications(realm => realm.All().Where(s => s.DeletePending && !s.Protected), deletedBeatmapSetsChanged); + subscriptionHiddenBeatmaps = realmFactory.RegisterForNotifications(realm => realm.All().Where(b => b.Hidden), beatmapsChanged); } private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index fc2188e597..c03d464ef6 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Select.Carousel ruleset.BindValueChanged(_ => { scoreSubscription?.Dispose(); - scoreSubscription = realmFactory.Register(realm => + scoreSubscription = realmFactory.RegisterForNotifications(realm => realm.All() .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 463f878e2a..5f288b972b 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (beatmapInfo == null) return; - scoreSubscription = realmFactory.Register(realm => + scoreSubscription = realmFactory.RegisterForNotifications(realm => realm.All() .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID), (_, changes, ___) => diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 904648f727..4abde56ea6 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Spectate playingUserStates.BindTo(spectatorClient.PlayingUserStates); playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); - realmSubscription = realmContextFactory.Register( + realmSubscription = realmContextFactory.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); foreach ((int id, var _) in userMap) From cb319cebdbae8cf70e4b750b1bd80ce655e41183 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 14:48:55 +0900 Subject: [PATCH 085/587] Refactor naming and add more comments to help understanding in `RealmContextFactory` subscription logic --- osu.Game/Database/RealmContextFactory.cs | 39 ++++++++++++++++-------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 697caf8cd3..0137e22e94 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -63,8 +63,22 @@ namespace osu.Game.Database private readonly ThreadLocal currentThreadCanCreateContexts = new ThreadLocal(); - private readonly Dictionary, IDisposable?> customSubscriptionActions = new Dictionary, IDisposable?>(); + /// + /// Holds a map of functions registered via and and a coinciding action which when triggered, + /// will unregister the subscription from realm. + /// + /// Put another way, the key is an action which registers the subscription with realm. The returned from the action is stored as the value and only + /// used internally. + /// + /// Entries in this dictionary are only removed when a consumer signals that the subscription should be permanently ceased (via their own ). + /// + private readonly Dictionary, IDisposable?> customSubscriptionsResetMap = new Dictionary, IDisposable?>(); + /// + /// Holds a map of functions registered via and a coinciding action which when triggered, + /// fires a change set event with an empty collection. This is used to inform subscribers when a realm context goes away, and ensure they don't use invalidated + /// managed realm objects from a previous firing. + /// private readonly Dictionary, Action> realmSubscriptionsResetMap = new Dictionary, Action>(); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)"); @@ -88,7 +102,7 @@ namespace osu.Game.Database Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}"); // Resubscribe any subscriptions - foreach (var action in customSubscriptionActions.Keys) + foreach (var action in customSubscriptionsResetMap.Keys) registerSubscription(action); } @@ -245,11 +259,12 @@ namespace osu.Game.Database lock (contextLock) { + Func action = realm => query(realm).QueryAsyncWithNotifications(onChanged); + + // Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing. realmSubscriptionsResetMap.Add(action, () => onChanged(new EmptyRealmSet(), null, null)); return RegisterCustomSubscription(action); } - - IDisposable? action(Realm realm) => query(realm).QueryAsyncWithNotifications(onChanged); } /// @@ -266,8 +281,8 @@ namespace osu.Game.Database registerSubscription(action); - // This token is returned to the consumer only. - // It will cause the registration to be permanently removed. + // This token is returned to the consumer. + // When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class). return new InvokeOnDisposal(() => { if (ThreadSafety.IsUpdateThread) @@ -279,10 +294,10 @@ namespace osu.Game.Database { lock (contextLock) { - if (customSubscriptionActions.TryGetValue(action, out var unsubscriptionAction)) + if (customSubscriptionsResetMap.TryGetValue(action, out var unsubscriptionAction)) { unsubscriptionAction?.Dispose(); - customSubscriptionActions.Remove(action); + customSubscriptionsResetMap.Remove(action); realmSubscriptionsResetMap.Remove(action); } } @@ -301,10 +316,10 @@ namespace osu.Game.Database lock (contextLock) { - Debug.Assert(!customSubscriptionActions.TryGetValue(action, out var found) || found == null); + Debug.Assert(!customSubscriptionsResetMap.TryGetValue(action, out var found) || found == null); current_thread_subscriptions_allowed.Value = true; - customSubscriptionActions[action] = action(realm); + customSubscriptionsResetMap[action] = action(realm); current_thread_subscriptions_allowed.Value = false; } } @@ -318,10 +333,10 @@ namespace osu.Game.Database foreach (var action in realmSubscriptionsResetMap.Values) action(); - foreach (var action in customSubscriptionActions) + foreach (var action in customSubscriptionsResetMap) { action.Value?.Dispose(); - customSubscriptionActions[action.Key] = null; + customSubscriptionsResetMap[action.Key] = null; } } From 1e483ece322f4c78c4ff8eab95c1993d388a1f9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 16:40:16 +0900 Subject: [PATCH 086/587] Avoid adding "exit all screens" step when running tests interactively --- osu.Game/Tests/Visual/ScreenTestScene.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index c44a848275..b6f6ca6daa 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -48,7 +49,11 @@ namespace osu.Game.Tests.Visual public virtual void SetUpSteps() => addExitAllScreensStep(); [TearDownSteps] - public virtual void TearDownSteps() => addExitAllScreensStep(); + public virtual void TearDownSteps() + { + if (DebugUtils.IsNUnitRunning) + addExitAllScreensStep(); + } private void addExitAllScreensStep() { From e22aea0613698af3fa5870fd745764306a9273dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 17:05:49 +0900 Subject: [PATCH 087/587] Apply same fix to `OsuGameTestScene` --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 6a11bd3fea..ebbd9bb5dd 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -72,8 +73,11 @@ namespace osu.Game.Tests.Visual [TearDownSteps] public void TearDownSteps() { - AddStep("exit game", () => Game.Exit()); - AddUntilStep("wait for game exit", () => Game.Parent == null); + if (DebugUtils.IsNUnitRunning) + { + AddStep("exit game", () => Game.Exit()); + AddUntilStep("wait for game exit", () => Game.Parent == null); + } } protected void CreateGame() From 161a2a321ef5ecc93dfeb9d5a99af66f7881be2a Mon Sep 17 00:00:00 2001 From: mk-56 Date: Mon, 24 Jan 2022 09:07:07 +0100 Subject: [PATCH 088/587] Remove bindable from ModeMultiplier --- osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs | 8 ++------ osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs | 8 ++------ osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs | 8 ++------ osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs | 8 ++------ osu.Game/Rulesets/Mods/ModFlashlight.cs | 2 +- 5 files changed, 9 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index e8f1ebdd10..4fbbf63abf 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -33,13 +33,9 @@ namespace osu.Game.Rulesets.Catch.Mods Precision = 0.1f }; - protected override BindableNumber ModeMultiplier { get; } = new BindableNumber - { - Default = 350, - Value = 350, - }; + protected override float ModeMultiplier => 350; - public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, ModeMultiplier.Value); + public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, ModeMultiplier); private CatchPlayfield playfield; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index 9ca1a72584..61f73a1ee5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -34,13 +34,9 @@ namespace osu.Game.Rulesets.Mania.Mods Precision = 0.1f }; - protected override BindableNumber ModeMultiplier { get; } = new BindableNumber - { - Default = 50, - Value = 50, - }; + protected override float ModeMultiplier => 50; - public override Flashlight CreateFlashlight() => new ManiaFlashlight(ChangeRadius.Value, InitialRadius.Value, ModeMultiplier.Value); + public override Flashlight CreateFlashlight() => new ManiaFlashlight(ChangeRadius.Value, InitialRadius.Value, ModeMultiplier); private class ManiaFlashlight : Flashlight { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index c6cf5ce4b5..75299863a9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -47,15 +47,11 @@ namespace osu.Game.Rulesets.Osu.Mods Precision = 0.1f }; - protected override BindableNumber ModeMultiplier { get; } = new BindableNumber - { - Default = 180, - Value = 180, - }; + protected override float ModeMultiplier => 180; private OsuFlashlight flashlight; - public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(ChangeRadius.Value, InitialRadius.Value, FollowDelay.Value, ModeMultiplier.Value); + public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(ChangeRadius.Value, InitialRadius.Value, FollowDelay.Value, ModeMultiplier); public void ApplyToDrawableHitObject(DrawableHitObject drawable) { diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index f235698b55..65a173b491 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -34,13 +34,9 @@ namespace osu.Game.Rulesets.Taiko.Mods Precision = 0.1f }; - protected override BindableNumber ModeMultiplier { get; } = new BindableNumber - { - Default = 250, - Value = 250, - }; + protected override float ModeMultiplier => 250; - public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, ModeMultiplier.Value); + public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, ModeMultiplier); private TaikoPlayfield playfield; diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index d16f310582..7b980e8097 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Initial radius", "Initial radius of the flashlight area.")] public abstract BindableNumber InitialRadius { get; } - protected abstract BindableNumber ModeMultiplier { get; } + protected abstract float ModeMultiplier { get; } } public abstract class ModFlashlight : ModFlashlight, IApplicableToDrawableRuleset, IApplicableToScoreProcessor From 52cd906af6552ecb099d40454d10e2e266a4e92f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 17:45:31 +0900 Subject: [PATCH 089/587] Move context retrieval inside lock --- osu.Game/Database/RealmContextFactory.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0137e22e94..be484329bc 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -309,13 +309,13 @@ namespace osu.Game.Database { Debug.Assert(ThreadSafety.IsUpdateThread); - // Retrieve context outside of flag update to ensure that the context is constructed, - // as attempting to access it inside the subscription if it's not constructed would lead to - // cyclic invocations of the subscription callback. - var realm = Context; - lock (contextLock) { + // Retrieve context outside of flag update to ensure that the context is constructed, + // as attempting to access it inside the subscription if it's not constructed would lead to + // cyclic invocations of the subscription callback. + var realm = Context; + Debug.Assert(!customSubscriptionsResetMap.TryGetValue(action, out var found) || found == null); current_thread_subscriptions_allowed.Value = true; From abf14f09820bb8f5859604135174f36f0e9e7cc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 17:46:53 +0900 Subject: [PATCH 090/587] Lock unregistration for sanity --- osu.Game/Database/RealmContextFactory.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index be484329bc..5dd4e88ccd 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -330,13 +330,16 @@ namespace osu.Game.Database /// private void unregisterAllSubscriptions() { - foreach (var action in realmSubscriptionsResetMap.Values) - action(); - - foreach (var action in customSubscriptionsResetMap) + lock (contextLock) { - action.Value?.Dispose(); - customSubscriptionsResetMap[action.Key] = null; + foreach (var action in realmSubscriptionsResetMap.Values) + action(); + + foreach (var action in customSubscriptionsResetMap) + { + action.Value?.Dispose(); + customSubscriptionsResetMap[action.Key] = null; + } } } From f4e7211ef1356f6598f4ad6038d896c771f39cbd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 17:52:36 +0900 Subject: [PATCH 091/587] Add xmldoc for `RegisterForNotifications` --- osu.Game/Database/RealmContextFactory.cs | 27 +++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 5dd4e88ccd..f8470b8c73 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -251,7 +251,28 @@ namespace osu.Game.Database } } - public IDisposable RegisterForNotifications(Func> query, NotificationCallbackDelegate onChanged) + /// + /// Subscribe to a realm collection and begin watching for asynchronous changes. + /// + /// + /// This adds osu! specific thread and managed state safety checks on top of . + /// + /// In addition to the documented realm behaviour, we have the additional requirement of handling subscriptions over potential context loss. + /// When this happens, callback events will be automatically fired: + /// - On context loss, a callback with an empty collection and null will be invoked. + /// - On context revival, a standard initial realm callback will arrive, with null and an up-to-date collection. + /// + /// The to observe for changes. + /// Type of the elements in the list. + /// + /// The callback to be invoked with the updated . + /// + /// A subscription token. It must be kept alive for as long as you want to receive change notifications. + /// To stop receiving notifications, call . + /// + /// May be null in the case the provided collection is not managed. + /// + public IDisposable RegisterForNotifications(Func> query, NotificationCallbackDelegate callback) where T : RealmObjectBase { if (!ThreadSafety.IsUpdateThread) @@ -259,10 +280,10 @@ namespace osu.Game.Database lock (contextLock) { - Func action = realm => query(realm).QueryAsyncWithNotifications(onChanged); + Func action = realm => query(realm).QueryAsyncWithNotifications(callback); // Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing. - realmSubscriptionsResetMap.Add(action, () => onChanged(new EmptyRealmSet(), null, null)); + realmSubscriptionsResetMap.Add(action, () => callback(new EmptyRealmSet(), null, null)); return RegisterCustomSubscription(action); } } From bf5bf8d1fd147f1a0e27756321512eaa0676e3d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 17:58:53 +0900 Subject: [PATCH 092/587] Rename dictionaries to match methods --- osu.Game/Database/RealmContextFactory.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index f8470b8c73..738d0a70a9 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -79,7 +79,7 @@ namespace osu.Game.Database /// fires a change set event with an empty collection. This is used to inform subscribers when a realm context goes away, and ensure they don't use invalidated /// managed realm objects from a previous firing. /// - private readonly Dictionary, Action> realmSubscriptionsResetMap = new Dictionary, Action>(); + private readonly Dictionary, Action> notificationsResetMap = new Dictionary, Action>(); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)"); @@ -283,7 +283,7 @@ namespace osu.Game.Database Func action = realm => query(realm).QueryAsyncWithNotifications(callback); // Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing. - realmSubscriptionsResetMap.Add(action, () => callback(new EmptyRealmSet(), null, null)); + notificationsResetMap.Add(action, () => callback(new EmptyRealmSet(), null, null)); return RegisterCustomSubscription(action); } } @@ -319,7 +319,7 @@ namespace osu.Game.Database { unsubscriptionAction?.Dispose(); customSubscriptionsResetMap.Remove(action); - realmSubscriptionsResetMap.Remove(action); + notificationsResetMap.Remove(action); } } } @@ -353,7 +353,7 @@ namespace osu.Game.Database { lock (contextLock) { - foreach (var action in realmSubscriptionsResetMap.Values) + foreach (var action in notificationsResetMap.Values) action(); foreach (var action in customSubscriptionsResetMap) From e3083c2477ae80b23f5c17c9360c7aa51b07dc88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 18:05:30 +0900 Subject: [PATCH 093/587] Fix copy pasted xmldoc --- osu.Game/Database/RealmContextFactory.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 738d0a70a9..3956738147 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -264,14 +264,12 @@ namespace osu.Game.Database /// /// The to observe for changes. /// Type of the elements in the list. - /// /// The callback to be invoked with the updated . /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . - /// - /// May be null in the case the provided collection is not managed. /// + /// public IDisposable RegisterForNotifications(Func> query, NotificationCallbackDelegate callback) where T : RealmObjectBase { From b0919722ac1c77fb484a7175e3f90cf325d03f9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 18:24:25 +0900 Subject: [PATCH 094/587] Guard against potential exception while blocking realm --- .../Sections/DebugSettings/MemorySettings.cs | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index f058c274e4..c5854981e6 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -1,11 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Localisation; @@ -52,30 +54,38 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings blockAction.Action = () => { - var blocking = realmFactory.BlockAllOperations(); - blockAction.Enabled.Value = false; - - // As a safety measure, unblock after 10 seconds. - // This is to handle the case where a dev may block, but then something on the update thread - // accesses realm and blocks for eternity. - Task.Factory.StartNew(() => + try { - Thread.Sleep(10000); - unblock(); - }); + var token = realmFactory.BlockAllOperations(); - unblockAction.Action = unblock; + blockAction.Enabled.Value = false; - void unblock() - { - blocking?.Dispose(); - blocking = null; - - Scheduler.Add(() => + // As a safety measure, unblock after 10 seconds. + // This is to handle the case where a dev may block, but then something on the update thread + // accesses realm and blocks for eternity. + Task.Factory.StartNew(() => { - blockAction.Enabled.Value = true; - unblockAction.Action = null; + Thread.Sleep(10000); + unblock(); }); + + unblockAction.Action = unblock; + + void unblock() + { + token?.Dispose(); + token = null; + + Scheduler.Add(() => + { + blockAction.Enabled.Value = true; + unblockAction.Action = null; + }); + } + } + catch (Exception e) + { + Logger.Error(e, "Blocking realm failed"); } }; } From 9afa034296e8f06642c907d44ab3061c076c9815 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 18:36:16 +0900 Subject: [PATCH 095/587] Fix attempt to revive update thread realm context from non-update thread --- osu.Game/Database/RealmContextFactory.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 3956738147..778092c543 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -594,7 +594,7 @@ namespace osu.Game.Database if (isDisposed) throw new ObjectDisposedException(nameof(RealmContextFactory)); - SynchronizationContext syncContext; + SynchronizationContext? syncContext = null; try { @@ -602,10 +602,20 @@ namespace osu.Game.Database lock (contextLock) { - if (!ThreadSafety.IsUpdateThread && context != null) - throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); + if (context == null) + { + // null context means the update thread has not yet retrieved its context. + // we don't need to worry about reviving the update context in this case, so don't bother with the SynchronizationContext. + Debug.Assert(!ThreadSafety.IsUpdateThread); + } + else + { + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); + + syncContext = SynchronizationContext.Current; + } - syncContext = SynchronizationContext.Current; unregisterAllSubscriptions(); Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); From 66c5d77d6388686c733eb229e822d0ad4120dee1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 18:55:15 +0900 Subject: [PATCH 096/587] Allow realm migration to run again if interrupted halfway --- osu.Game/Database/EFToRealmMigrator.cs | 258 ++++++++++++------------- osu.Game/Screens/Loader.cs | 15 +- 2 files changed, 140 insertions(+), 133 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index bbbdac352e..edce99e302 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -27,7 +27,9 @@ namespace osu.Game.Database { internal class EFToRealmMigrator : CompositeDrawable { - public bool FinishedMigrating { get; private set; } + public Task MigrationCompleted => migrationCompleted.Task; + + private readonly TaskCompletionSource migrationCompleted = new TaskCompletionSource(); [Resolved] private DatabaseContextFactory efContextFactory { get; set; } = null!; @@ -99,6 +101,17 @@ namespace osu.Game.Database { using (var ef = efContextFactory.Get()) { + realmContextFactory.Write(realm => + { + // Before beginning, ensure realm is in an empty state. + // Migrations which are half-completed could lead to issues if the user tries a second time. + // Note that we only do this for beatmaps and scores since the other migrations are yonks old. + realm.RemoveAll(); + realm.RemoveAll(); + realm.RemoveAll(); + realm.RemoveAll(); + }); + migrateSettings(ef); migrateSkins(ef); migrateBeatmaps(ef); @@ -114,7 +127,7 @@ namespace osu.Game.Database Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important); }, TaskCreationOptions.LongRunning).ContinueWith(t => { - FinishedMigrating = true; + migrationCompleted.SetResult(true); }); } @@ -149,87 +162,78 @@ namespace osu.Game.Database { log($"Found {count} beatmaps in EF"); - // only migrate data if the realm database is empty. - // note that this cannot be written as: `realm.All().All(s => s.Protected)`, because realm does not support `.All()`. - if (realm.All().Any(s => !s.Protected)) - { - log("Skipping migration as realm already has beatmaps loaded"); - } - else - { - var transaction = realm.BeginWrite(); - int written = 0; + var transaction = realm.BeginWrite(); + int written = 0; - try + try + { + foreach (var beatmapSet in existingBeatmapSets) { - foreach (var beatmapSet in existingBeatmapSets) + if (++written % 1000 == 0) { - if (++written % 1000 == 0) - { - transaction.Commit(); - transaction = realm.BeginWrite(); - log($"Migrated {written}/{count} beatmaps..."); - } + transaction.Commit(); + transaction = realm.BeginWrite(); + log($"Migrated {written}/{count} beatmaps..."); + } - var realmBeatmapSet = new BeatmapSetInfo + var realmBeatmapSet = new BeatmapSetInfo + { + OnlineID = beatmapSet.OnlineID ?? -1, + DateAdded = beatmapSet.DateAdded, + Status = beatmapSet.Status, + DeletePending = beatmapSet.DeletePending, + Hash = beatmapSet.Hash, + Protected = beatmapSet.Protected, + }; + + migrateFiles(beatmapSet, realm, realmBeatmapSet); + + foreach (var beatmap in beatmapSet.Beatmaps) + { + var ruleset = realm.Find(beatmap.RulesetInfo.ShortName); + var metadata = getBestMetadata(beatmap.Metadata, beatmapSet.Metadata); + + var realmBeatmap = new BeatmapInfo(ruleset, new BeatmapDifficulty(beatmap.BaseDifficulty), metadata) { - OnlineID = beatmapSet.OnlineID ?? -1, - DateAdded = beatmapSet.DateAdded, - Status = beatmapSet.Status, - DeletePending = beatmapSet.DeletePending, - Hash = beatmapSet.Hash, - Protected = beatmapSet.Protected, + DifficultyName = beatmap.DifficultyName, + Status = beatmap.Status, + OnlineID = beatmap.OnlineID ?? -1, + Length = beatmap.Length, + BPM = beatmap.BPM, + Hash = beatmap.Hash, + StarRating = beatmap.StarRating, + MD5Hash = beatmap.MD5Hash, + Hidden = beatmap.Hidden, + AudioLeadIn = beatmap.AudioLeadIn, + StackLeniency = beatmap.StackLeniency, + SpecialStyle = beatmap.SpecialStyle, + LetterboxInBreaks = beatmap.LetterboxInBreaks, + WidescreenStoryboard = beatmap.WidescreenStoryboard, + EpilepsyWarning = beatmap.EpilepsyWarning, + SamplesMatchPlaybackRate = beatmap.SamplesMatchPlaybackRate, + DistanceSpacing = beatmap.DistanceSpacing, + BeatDivisor = beatmap.BeatDivisor, + GridSize = beatmap.GridSize, + TimelineZoom = beatmap.TimelineZoom, + Countdown = beatmap.Countdown, + CountdownOffset = beatmap.CountdownOffset, + MaxCombo = beatmap.MaxCombo, + Bookmarks = beatmap.Bookmarks, + BeatmapSet = realmBeatmapSet, }; - migrateFiles(beatmapSet, realm, realmBeatmapSet); - - foreach (var beatmap in beatmapSet.Beatmaps) - { - var ruleset = realm.Find(beatmap.RulesetInfo.ShortName); - var metadata = getBestMetadata(beatmap.Metadata, beatmapSet.Metadata); - - var realmBeatmap = new BeatmapInfo(ruleset, new BeatmapDifficulty(beatmap.BaseDifficulty), metadata) - { - DifficultyName = beatmap.DifficultyName, - Status = beatmap.Status, - OnlineID = beatmap.OnlineID ?? -1, - Length = beatmap.Length, - BPM = beatmap.BPM, - Hash = beatmap.Hash, - StarRating = beatmap.StarRating, - MD5Hash = beatmap.MD5Hash, - Hidden = beatmap.Hidden, - AudioLeadIn = beatmap.AudioLeadIn, - StackLeniency = beatmap.StackLeniency, - SpecialStyle = beatmap.SpecialStyle, - LetterboxInBreaks = beatmap.LetterboxInBreaks, - WidescreenStoryboard = beatmap.WidescreenStoryboard, - EpilepsyWarning = beatmap.EpilepsyWarning, - SamplesMatchPlaybackRate = beatmap.SamplesMatchPlaybackRate, - DistanceSpacing = beatmap.DistanceSpacing, - BeatDivisor = beatmap.BeatDivisor, - GridSize = beatmap.GridSize, - TimelineZoom = beatmap.TimelineZoom, - Countdown = beatmap.Countdown, - CountdownOffset = beatmap.CountdownOffset, - MaxCombo = beatmap.MaxCombo, - Bookmarks = beatmap.Bookmarks, - BeatmapSet = realmBeatmapSet, - }; - - realmBeatmapSet.Beatmaps.Add(realmBeatmap); - } - - realm.Add(realmBeatmapSet); + realmBeatmapSet.Beatmaps.Add(realmBeatmap); } - } - finally - { - transaction.Commit(); - } - log($"Successfully migrated {count} beatmaps to realm"); + realm.Add(realmBeatmapSet); + } } + finally + { + transaction.Commit(); + } + + log($"Successfully migrated {count} beatmaps to realm"); }); } @@ -280,70 +284,62 @@ namespace osu.Game.Database { log($"Found {count} scores in EF"); - // only migrate data if the realm database is empty. - if (realm.All().Any()) - { - log("Skipping migration as realm already has scores loaded"); - } - else - { - var transaction = realm.BeginWrite(); - int written = 0; + var transaction = realm.BeginWrite(); + int written = 0; - try + try + { + foreach (var score in existingScores) { - foreach (var score in existingScores) + if (++written % 1000 == 0) { - if (++written % 1000 == 0) - { - transaction.Commit(); - transaction = realm.BeginWrite(); - log($"Migrated {written}/{count} scores..."); - } - - var beatmap = realm.All().First(b => b.Hash == score.BeatmapInfo.Hash); - var ruleset = realm.Find(score.Ruleset.ShortName); - var user = new RealmUser - { - OnlineID = score.User.OnlineID, - Username = score.User.Username - }; - - var realmScore = new ScoreInfo(beatmap, ruleset, user) - { - Hash = score.Hash, - DeletePending = score.DeletePending, - OnlineID = score.OnlineID ?? -1, - ModsJson = score.ModsJson, - StatisticsJson = score.StatisticsJson, - TotalScore = score.TotalScore, - MaxCombo = score.MaxCombo, - Accuracy = score.Accuracy, - HasReplay = ((IScoreInfo)score).HasReplay, - Date = score.Date, - PP = score.PP, - Rank = score.Rank, - HitEvents = score.HitEvents, - Passed = score.Passed, - Combo = score.Combo, - Position = score.Position, - Statistics = score.Statistics, - Mods = score.Mods, - APIMods = score.APIMods, - }; - - migrateFiles(score, realm, realmScore); - - realm.Add(realmScore); + transaction.Commit(); + transaction = realm.BeginWrite(); + log($"Migrated {written}/{count} scores..."); } - } - finally - { - transaction.Commit(); - } - log($"Successfully migrated {count} scores to realm"); + var beatmap = realm.All().First(b => b.Hash == score.BeatmapInfo.Hash); + var ruleset = realm.Find(score.Ruleset.ShortName); + var user = new RealmUser + { + OnlineID = score.User.OnlineID, + Username = score.User.Username + }; + + var realmScore = new ScoreInfo(beatmap, ruleset, user) + { + Hash = score.Hash, + DeletePending = score.DeletePending, + OnlineID = score.OnlineID ?? -1, + ModsJson = score.ModsJson, + StatisticsJson = score.StatisticsJson, + TotalScore = score.TotalScore, + MaxCombo = score.MaxCombo, + Accuracy = score.Accuracy, + HasReplay = ((IScoreInfo)score).HasReplay, + Date = score.Date, + PP = score.PP, + Rank = score.Rank, + HitEvents = score.HitEvents, + Passed = score.Passed, + Combo = score.Combo, + Position = score.Position, + Statistics = score.Statistics, + Mods = score.Mods, + APIMods = score.APIMods, + }; + + migrateFiles(score, realm, realmScore); + + realm.Add(realmScore); + } } + finally + { + transaction.Commit(); + } + + log($"Successfully migrated {count} scores to realm"); }); } diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 8c4a13f2bd..a72ba89dfa 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -74,11 +74,22 @@ namespace osu.Game.Screens base.OnEntering(last); LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal); - LoadComponentAsync(loadableScreen = CreateLoadableScreen()); // A non-null context factory means there's still content to migrate. if (efContextFactory != null) + { LoadComponentAsync(realmMigrator = new EFToRealmMigrator(), AddInternal); + realmMigrator.MigrationCompleted.ContinueWith(_ => Schedule(() => + { + // Delay initial screen loading to ensure that the migration is in a complete and sane state + // before the intro screen may import the game intro beatmap. + LoadComponentAsync(loadableScreen = CreateLoadableScreen()); + })); + } + else + { + LoadComponentAsync(loadableScreen = CreateLoadableScreen()); + } LoadComponentAsync(spinner = new LoadingSpinner(true, true) { @@ -96,7 +107,7 @@ namespace osu.Game.Screens private void checkIfLoaded() { - if (loadableScreen.LoadState != LoadState.Ready || !precompiler.FinishedCompiling || realmMigrator?.FinishedMigrating == false) + if (loadableScreen?.LoadState != LoadState.Ready || !precompiler.FinishedCompiling) { Schedule(checkIfLoaded); return; From 948867898cb36ed2a32ff2e58a6f7c9de64d66d5 Mon Sep 17 00:00:00 2001 From: mk-56 Date: Mon, 24 Jan 2022 11:38:52 +0100 Subject: [PATCH 097/587] ModeMultiplier rename --- osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs | 8 ++++---- osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs | 8 ++++---- osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs | 8 ++++---- osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs | 8 ++++---- osu.Game/Rulesets/Mods/ModFlashlight.cs | 14 ++++++-------- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index 4fbbf63abf..d48382a9ee 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Catch.Mods Precision = 0.1f }; - protected override float ModeMultiplier => 350; + protected virtual float DefaultFlashlightSize => 350; - public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, ModeMultiplier); + public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, DefaultFlashlightSize); private CatchPlayfield playfield; @@ -49,8 +49,8 @@ namespace osu.Game.Rulesets.Catch.Mods { private readonly CatchPlayfield playfield; - public CatchFlashlight(CatchPlayfield playfield, bool isRadiusBasedOnCombo, float initialRadius, float modeMultiplier) - : base(isRadiusBasedOnCombo, initialRadius, modeMultiplier) + public CatchFlashlight(CatchPlayfield playfield, bool isRadiusBasedOnCombo, float initialRadius, float defaultFlashlightSize) + : base(isRadiusBasedOnCombo, initialRadius, defaultFlashlightSize) { this.playfield = playfield; FlashlightSize = new Vector2(0, GetRadiusFor(0)); diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index 61f73a1ee5..eb3f60edce 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -34,16 +34,16 @@ namespace osu.Game.Rulesets.Mania.Mods Precision = 0.1f }; - protected override float ModeMultiplier => 50; + protected virtual float DefaultFlashlightSize => 50; - public override Flashlight CreateFlashlight() => new ManiaFlashlight(ChangeRadius.Value, InitialRadius.Value, ModeMultiplier); + public override Flashlight CreateFlashlight() => new ManiaFlashlight(ChangeRadius.Value, InitialRadius.Value, DefaultFlashlightSize); private class ManiaFlashlight : Flashlight { private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); - public ManiaFlashlight(bool isRadiusBasedOnCombo, float initialRadius, float modeMultiplier) - : base(isRadiusBasedOnCombo, initialRadius, modeMultiplier) + public ManiaFlashlight(bool isRadiusBasedOnCombo, float initialRadius, float defaultFlashlightSize) + : base(isRadiusBasedOnCombo, initialRadius, defaultFlashlightSize) { FlashlightSize = new Vector2(DrawWidth, GetRadiusFor(0)); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index 75299863a9..6a9d199c54 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -47,11 +47,11 @@ namespace osu.Game.Rulesets.Osu.Mods Precision = 0.1f }; - protected override float ModeMultiplier => 180; + protected virtual float DefaultFlashlightSize => 180; private OsuFlashlight flashlight; - public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(ChangeRadius.Value, InitialRadius.Value, FollowDelay.Value, ModeMultiplier); + public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(ChangeRadius.Value, InitialRadius.Value, FollowDelay.Value, DefaultFlashlightSize); public void ApplyToDrawableHitObject(DrawableHitObject drawable) { @@ -66,8 +66,8 @@ namespace osu.Game.Rulesets.Osu.Mods //public float InitialRadius { private get; set; } public bool ChangeRadius { private get; set; } - public OsuFlashlight(bool isRadiusBasedOnCombo, float initialRadius, double followDelay, float modeMultiplier) - : base(isRadiusBasedOnCombo, initialRadius, modeMultiplier) + public OsuFlashlight(bool isRadiusBasedOnCombo, float initialRadius, double followDelay, float defaultFlashlightSize) + : base(isRadiusBasedOnCombo, initialRadius, defaultFlashlightSize) { FollowDelay = followDelay; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index 65a173b491..8de7c859c4 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Taiko.Mods Precision = 0.1f }; - protected override float ModeMultiplier => 250; + protected virtual float DefaultFlashlightSize => 250; - public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, ModeMultiplier); + public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, DefaultFlashlightSize); private TaikoPlayfield playfield; @@ -51,8 +51,8 @@ namespace osu.Game.Rulesets.Taiko.Mods private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); private readonly TaikoPlayfield taikoPlayfield; - public TaikoFlashlight(TaikoPlayfield taikoPlayfield, bool isRadiusBasedOnCombo, float initialRadius, float modeMultiplier) - : base(isRadiusBasedOnCombo, initialRadius, modeMultiplier) + public TaikoFlashlight(TaikoPlayfield taikoPlayfield, bool isRadiusBasedOnCombo, float initialRadius, float defaultFlashlightSize) + : base(isRadiusBasedOnCombo, initialRadius, defaultFlashlightSize) { this.taikoPlayfield = taikoPlayfield; FlashlightSize = getSizeFor(0); diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 7b980e8097..531ee92b7a 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -38,8 +38,6 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Initial radius", "Initial radius of the flashlight area.")] public abstract BindableNumber InitialRadius { get; } - - protected abstract float ModeMultiplier { get; } } public abstract class ModFlashlight : ModFlashlight, IApplicableToDrawableRuleset, IApplicableToScoreProcessor @@ -102,13 +100,13 @@ namespace osu.Game.Rulesets.Mods public readonly float InitialRadius; - public readonly float ModeMultiplier; + public readonly float DefaultFlashlightSize; - protected Flashlight(bool isRadiusBasedOnCombo, float initialRadius, float modeMultiplier) + protected Flashlight(bool isRadiusBasedOnCombo, float initialRadius, float defaultFlashlightSize) { IsRadiusBasedOnCombo = isRadiusBasedOnCombo; InitialRadius = initialRadius; - ModeMultiplier = modeMultiplier; + DefaultFlashlightSize = defaultFlashlightSize; } [BackgroundDependencyLoader] @@ -147,12 +145,12 @@ namespace osu.Game.Rulesets.Mods if (IsRadiusBasedOnCombo) { if (combo > 200) - return InitialRadius * 0.8f * ModeMultiplier; + return InitialRadius * 0.8f * DefaultFlashlightSize; else if (combo > 100) - return InitialRadius * 0.9f * ModeMultiplier; + return InitialRadius * 0.9f * DefaultFlashlightSize; } - return InitialRadius * ModeMultiplier; + return InitialRadius * DefaultFlashlightSize; } private Vector2 flashlightPosition; From deaff340d2733c544b8e87d78a51d5d69063ee27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 17:58:06 +0900 Subject: [PATCH 098/587] Add test coverage of saving velocity --- .../Editor/TestSceneSliderVelocityAdjust.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs new file mode 100644 index 0000000000..4750c97566 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Input; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components.Timeline; +using osu.Game.Screens.Edit.Timing; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public class TestSceneSliderVelocityAdjust : OsuGameTestScene + { + private Screens.Edit.Editor editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor; + + private EditorBeatmap editorBeatmap => editor.ChildrenOfType().FirstOrDefault(); + + private EditorClock editorClock => editor.ChildrenOfType().FirstOrDefault(); + + private Slider slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); + + private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault(); + + private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType().First(); + + private IndeterminateSliderWithTextBoxInput velocityTextBox => Game.ChildrenOfType().First().ChildrenOfType>().First(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + private bool editorComponentsReady => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true + && editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true + && editor?.ChildrenOfType().FirstOrDefault()?.IsLoaded == true; + + [TestCase(true)] + [TestCase(false)] + public void TestVelocityChangeSavesCorrectly(bool adjustVelocity) + { + double? velocity = null; + + AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader())); + AddUntilStep("wait for editor load", () => editorComponentsReady); + + AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time)); + AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); + + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(editor.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre)); + AddStep("start placement", () => InputManager.Click(MouseButton.Left)); + + AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType().First().ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); + AddStep("end placement", () => InputManager.Click(MouseButton.Right)); + + AddStep("exit placement mode", () => InputManager.Key(Key.Number1)); + + AddAssert("slider placed", () => slider != null); + + AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider)); + + AddAssert("ensure one slider placed", () => slider != null); + + AddStep("store velocity", () => velocity = slider.Velocity); + + if (adjustVelocity) + { + AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick()); + AddStep("change velocity", () => velocityTextBox.Current.Value = 2); + + AddAssert("velocity adjusted", () => + { + Debug.Assert(velocity != null); + return Precision.AlmostEquals(velocity.Value * 2, slider.Velocity); + }); + + AddStep("store velocity", () => velocity = slider.Velocity); + } + + AddStep("save", () => InputManager.Keys(PlatformAction.Save)); + AddStep("exit", () => InputManager.Key(Key.Escape)); + + AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader())); + AddUntilStep("wait for editor load", () => editorComponentsReady); + + AddStep("seek to slider", () => editorClock.Seek(slider.StartTime)); + AddAssert("slider has correct velocity", () => slider.Velocity == velocity); + } + } +} From c3758047fd9e2113173ea9bfcba50a575f5b7035 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 19:54:07 +0900 Subject: [PATCH 099/587] Don't include nested hit objects' `DifficultyControLPoints` in legacy encoder logic The editor doesn't currently propagate velocity to nested objects. We're not yet sure whether it should or not. For now, let's just ignore nested objects' `DifficultyControlPoints` for simplicity. Note that this only affects osu! ruleset due to the pre-check on `isOsuRuleset`. --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 4cf6d3335f..9d848fd8a4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -242,12 +242,7 @@ namespace osu.Game.Beatmaps.Formats yield break; foreach (var hitObject in hitObjects) - { yield return hitObject.DifficultyControlPoint; - - foreach (var nested in collectDifficultyControlPoints(hitObject.NestedHitObjects)) - yield return nested; - } } void extractDifficultyControlPoints(IEnumerable hitObjects) From 6eb2c28e41369be24fbefb14478b62935207992d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 19:59:58 +0900 Subject: [PATCH 100/587] Rename `RealmContextFactory` to `RealmAccess` --- osu.Game.Benchmarks/BenchmarkRealmReads.cs | 20 +- .../Beatmaps/IO/BeatmapImportHelper.cs | 4 +- .../Database/BeatmapImporterTests.cs | 258 +++++++++--------- osu.Game.Tests/Database/FileStoreTests.cs | 24 +- osu.Game.Tests/Database/GeneralUsageTests.cs | 22 +- osu.Game.Tests/Database/RealmLiveTests.cs | 60 ++-- .../RealmSubscriptionRegistrationTests.cs | 42 +-- osu.Game.Tests/Database/RealmTest.cs | 34 +-- osu.Game.Tests/Database/RulesetStoreTests.cs | 18 +- .../Database/TestRealmKeyBindingStore.cs | 16 +- .../Gameplay/TestSceneStoryboardSamples.cs | 2 +- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 20 +- .../Background/TestSceneUserDimBackgrounds.cs | 6 +- .../TestSceneManageCollectionsDialog.cs | 6 +- .../Visual/Multiplayer/QueueModeTestScene.cs | 6 +- .../TestSceneDrawableRoomPlaylist.cs | 6 +- .../Multiplayer/TestSceneMultiplayer.cs | 6 +- .../TestSceneMultiplayerMatchSongSelect.cs | 6 +- .../TestSceneMultiplayerMatchSubScreen.cs | 6 +- .../TestSceneMultiplayerPlaylist.cs | 6 +- .../TestSceneMultiplayerQueueList.cs | 6 +- .../TestSceneMultiplayerReadyButton.cs | 6 +- .../TestSceneMultiplayerSpectateButton.cs | 6 +- .../TestScenePlaylistsSongSelect.cs | 6 +- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 6 +- .../TestSceneChangeAndUseGameplayBindings.cs | 2 +- .../TestScenePlaylistsRoomCreation.cs | 6 +- .../Visual/Ranking/TestSceneResultsScreen.cs | 4 +- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 8 +- .../SongSelect/TestSceneFilterControl.cs | 6 +- .../SongSelect/TestScenePlaySongSelect.cs | 6 +- .../SongSelect/TestSceneTopLocalRank.cs | 8 +- .../TestSceneDeleteLocalScore.cs | 12 +- osu.Game/Beatmaps/BeatmapManager.cs | 30 +- osu.Game/Beatmaps/BeatmapModelManager.cs | 8 +- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- osu.Game/Configuration/SettingsStore.cs | 6 +- osu.Game/Database/EFToRealmMigrator.cs | 12 +- ...{RealmContextFactory.cs => RealmAccess.cs} | 10 +- osu.Game/Database/RealmLive.cs | 14 +- osu.Game/Database/RealmObjectExtensions.cs | 12 +- osu.Game/IO/IStorageResourceProvider.cs | 2 +- .../Bindings/DatabasedKeyBindingContainer.cs | 8 +- osu.Game/Input/RealmKeyBindingStore.cs | 10 +- osu.Game/Online/BeatmapDownloadTracker.cs | 4 +- .../OnlinePlayBeatmapAvailabilityTracker.cs | 10 +- osu.Game/Online/ScoreDownloadTracker.cs | 4 +- osu.Game/OsuGameBase.cs | 22 +- osu.Game/Overlays/MusicController.cs | 10 +- .../Sections/DebugSettings/MemorySettings.cs | 6 +- .../Settings/Sections/Input/KeyBindingRow.cs | 4 +- .../Sections/Input/KeyBindingsSubsection.cs | 8 +- .../Overlays/Settings/Sections/SkinSection.cs | 14 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 4 +- .../Configuration/RulesetConfigManager.cs | 12 +- osu.Game/Rulesets/RulesetConfigCache.cs | 8 +- osu.Game/Rulesets/RulesetStore.cs | 8 +- osu.Game/Scoring/ScoreManager.cs | 12 +- osu.Game/Scoring/ScoreModelManager.cs | 6 +- osu.Game/Screens/Menu/IntroScreen.cs | 6 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 14 +- .../Screens/Select/Carousel/TopLocalRank.cs | 4 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 6 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 4 +- osu.Game/Skinning/Skin.cs | 4 +- osu.Game/Skinning/SkinManager.cs | 22 +- osu.Game/Skinning/SkinModelManager.cs | 6 +- osu.Game/Stores/BeatmapImporter.cs | 6 +- osu.Game/Stores/RealmArchiveModelImporter.cs | 16 +- osu.Game/Stores/RealmArchiveModelManager.cs | 12 +- osu.Game/Stores/RealmFileStore.cs | 8 +- .../Drawables/DrawableStoryboard.cs | 4 +- .../Tests/Beatmaps/HitObjectSampleTest.cs | 2 +- osu.Game/Tests/Visual/EditorTestScene.cs | 14 +- osu.Game/Tests/Visual/OsuTestScene.cs | 6 +- osu.Game/Tests/Visual/SkinnableTestScene.cs | 2 +- 76 files changed, 516 insertions(+), 516 deletions(-) rename osu.Game/Database/{RealmContextFactory.cs => RealmAccess.cs} (98%) diff --git a/osu.Game.Benchmarks/BenchmarkRealmReads.cs b/osu.Game.Benchmarks/BenchmarkRealmReads.cs index bb22fab51c..412f86bd1c 100644 --- a/osu.Game.Benchmarks/BenchmarkRealmReads.cs +++ b/osu.Game.Benchmarks/BenchmarkRealmReads.cs @@ -16,7 +16,7 @@ namespace osu.Game.Benchmarks public class BenchmarkRealmReads : BenchmarkTest { private TemporaryNativeStorage storage; - private RealmContextFactory realmFactory; + private RealmAccess realm; private UpdateThread updateThread; [Params(1, 100, 1000)] @@ -27,9 +27,9 @@ namespace osu.Game.Benchmarks storage = new TemporaryNativeStorage("realm-benchmark"); storage.DeleteDirectory(string.Empty); - realmFactory = new RealmContextFactory(storage, "client"); + realm = new RealmAccess(storage, "client"); - realmFactory.Run(realm => + realm.Run(realm => { realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo }))); }); @@ -41,7 +41,7 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkDirectPropertyRead() { - realmFactory.Run(realm => + realm.Run(realm => { var beatmapSet = realm.All().First(); @@ -61,7 +61,7 @@ namespace osu.Game.Benchmarks { try { - var beatmapSet = realmFactory.Context.All().First(); + var beatmapSet = realm.Realm.All().First(); for (int i = 0; i < ReadsPerFetch; i++) { @@ -80,9 +80,9 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkRealmLivePropertyRead() { - realmFactory.Run(realm => + realm.Run(r => { - var beatmapSet = realm.All().First().ToLive(realmFactory); + var beatmapSet = r.All().First().ToLive(realm); for (int i = 0; i < ReadsPerFetch; i++) { @@ -100,7 +100,7 @@ namespace osu.Game.Benchmarks { try { - var beatmapSet = realmFactory.Context.All().First().ToLive(realmFactory); + var beatmapSet = realm.Realm.All().First().ToLive(realm); for (int i = 0; i < ReadsPerFetch; i++) { @@ -119,7 +119,7 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkDetachedPropertyRead() { - realmFactory.Run(realm => + realm.Run(realm => { var beatmapSet = realm.All().First().Detach(); @@ -133,7 +133,7 @@ namespace osu.Game.Benchmarks [GlobalCleanup] public void Cleanup() { - realmFactory?.Dispose(); + realm?.Dispose(); storage?.Dispose(); updateThread?.Exit(); } diff --git a/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs index 7aa2dc7093..9e440c6bce 100644 --- a/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs +++ b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs @@ -53,9 +53,9 @@ namespace osu.Game.Tests.Beatmaps.IO private static void ensureLoaded(OsuGameBase osu, int timeout = 60000) { - var realmContextFactory = osu.Dependencies.Get(); + var realm = osu.Dependencies.Get(); - realmContextFactory.Run(realm => BeatmapImporterTests.EnsureLoaded(realm, timeout)); + realm.Run(r => BeatmapImporterTests.EnsureLoaded(r, timeout)); // TODO: add back some extra checks outside of the realm ones? // var set = queryBeatmapSets().First(); diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 227314cffd..a52b21244c 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -38,10 +38,10 @@ namespace osu.Game.Tests.Database [Test] public void TestDetachBeatmapSet() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using (var importer = new BeatmapModelManager(realmFactory, storage)) - using (new RulesetStore(realmFactory, storage)) + using (var importer = new BeatmapModelManager(realm, storage)) + using (new RulesetStore(realm, storage)) { ILive? beatmapSet; @@ -82,10 +82,10 @@ namespace osu.Game.Tests.Database [Test] public void TestUpdateDetachedBeatmapSet() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using (var importer = new BeatmapModelManager(realmFactory, storage)) - using (new RulesetStore(realmFactory, storage)) + using (var importer = new BeatmapModelManager(realm, storage)) + using (new RulesetStore(realm, storage)) { ILive? beatmapSet; @@ -139,53 +139,53 @@ namespace osu.Game.Tests.Database [Test] public void TestImportBeatmapThenCleanup() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using (var importer = new BeatmapModelManager(realmFactory, storage)) - using (new RulesetStore(realmFactory, storage)) + using (var importer = new BeatmapModelManager(realm, storage)) + using (new RulesetStore(realm, storage)) { ILive? imported; using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) imported = await importer.Import(reader); - Assert.AreEqual(1, realmFactory.Context.All().Count()); + Assert.AreEqual(1, realm.Realm.All().Count()); Assert.NotNull(imported); Debug.Assert(imported != null); imported.PerformWrite(s => s.DeletePending = true); - Assert.AreEqual(1, realmFactory.Context.All().Count(s => s.DeletePending)); + Assert.AreEqual(1, realm.Realm.All().Count(s => s.DeletePending)); } }); Logger.Log("Running with no work to purge pending deletions"); - RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All().Count()); }); + RunTestWithRealm((realm, _) => { Assert.AreEqual(0, realm.Realm.All().Count()); }); } [Test] public void TestImportWhenClosed() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - await LoadOszIntoStore(importer, realmFactory.Context); + await LoadOszIntoStore(importer, realm.Realm); }); } [Test] public void TestAccessFileAfterImport() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); var beatmap = imported.Beatmaps.First(); var file = beatmap.File; @@ -198,24 +198,24 @@ namespace osu.Game.Tests.Database [Test] public void TestImportThenDelete() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); - deleteBeatmapSet(imported, realmFactory.Context); + deleteBeatmapSet(imported, realm.Realm); }); } [Test] public void TestImportThenDeleteFromStream() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? tempPath = TestResources.GetTestBeatmapForImport(); @@ -224,7 +224,7 @@ namespace osu.Game.Tests.Database using (var stream = File.OpenRead(tempPath)) { importedSet = await importer.Import(new ImportTask(stream, Path.GetFileName(tempPath))); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); } Assert.NotNull(importedSet); @@ -233,39 +233,39 @@ namespace osu.Game.Tests.Database Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing"); File.Delete(tempPath); - var imported = realmFactory.Context.All().First(beatmapSet => beatmapSet.ID == importedSet.ID); + var imported = realm.Realm.All().First(beatmapSet => beatmapSet.ID == importedSet.ID); - deleteBeatmapSet(imported, realmFactory.Context); + deleteBeatmapSet(imported, realm.Realm); }); } [Test] public void TestImportThenImport() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); - var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); + var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); - checkBeatmapSetCount(realmFactory.Context, 1); - checkSingleReferencedFileCount(realmFactory.Context, 18); + checkBeatmapSetCount(realm.Realm, 1); + checkSingleReferencedFileCount(realm.Realm, 18); }); } [Test] public void TestImportThenImportWithReZip() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -274,7 +274,7 @@ namespace osu.Game.Tests.Database try { - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); string hashBefore = hashFile(temp); @@ -292,7 +292,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); Assert.NotNull(importedSecondTime); Debug.Assert(importedSecondTime != null); @@ -311,10 +311,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportThenImportWithChangedHashedFile() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -323,9 +323,9 @@ namespace osu.Game.Tests.Database try { - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); - await createScoreForBeatmap(realmFactory.Context, imported.Beatmaps.First()); + await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First()); using (var zip = ZipArchive.Open(temp)) zip.WriteToDirectory(extractedFolder); @@ -343,7 +343,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); // check the newly "imported" beatmap is not the original. Assert.NotNull(importedSecondTime); @@ -363,10 +363,10 @@ namespace osu.Game.Tests.Database [Ignore("intentionally broken by import optimisations")] public void TestImportThenImportWithChangedFile() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -375,7 +375,7 @@ namespace osu.Game.Tests.Database try { - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); using (var zip = ZipArchive.Open(temp)) zip.WriteToDirectory(extractedFolder); @@ -392,7 +392,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); Assert.NotNull(importedSecondTime); Debug.Assert(importedSecondTime != null); @@ -411,10 +411,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportThenImportWithDifferentFilename() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -423,7 +423,7 @@ namespace osu.Game.Tests.Database try { - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); using (var zip = ZipArchive.Open(temp)) zip.WriteToDirectory(extractedFolder); @@ -440,7 +440,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); Assert.NotNull(importedSecondTime); Debug.Assert(importedSecondTime != null); @@ -460,12 +460,12 @@ namespace osu.Game.Tests.Database [Ignore("intentionally broken by import optimisations")] public void TestImportCorruptThenImport() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); var firstFile = imported.Files.First(); @@ -476,7 +476,7 @@ namespace osu.Game.Tests.Database using (var stream = storage.GetStream(firstFile.File.GetStoragePath(), FileAccess.Write, FileMode.Create)) stream.WriteByte(0); - var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); using (var stream = storage.GetStream(firstFile.File.GetStoragePath())) Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import"); @@ -485,18 +485,18 @@ namespace osu.Game.Tests.Database Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); - checkBeatmapSetCount(realmFactory.Context, 1); - checkSingleReferencedFileCount(realmFactory.Context, 18); + checkBeatmapSetCount(realm.Realm, 1); + checkSingleReferencedFileCount(realm.Realm, 18); }); } [Test] public void TestModelCreationFailureDoesntReturn() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); var progressNotification = new ImportProgressNotification(); @@ -510,8 +510,8 @@ namespace osu.Game.Tests.Database new ImportTask(zipStream, string.Empty) ); - checkBeatmapSetCount(realmFactory.Context, 0); - checkBeatmapCount(realmFactory.Context, 0); + checkBeatmapSetCount(realm.Realm, 0); + checkBeatmapCount(realm.Realm, 0); Assert.IsEmpty(imported); Assert.AreEqual(ProgressNotificationState.Cancelled, progressNotification.State); @@ -521,7 +521,7 @@ namespace osu.Game.Tests.Database [Test] public void TestRollbackOnFailure() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { int loggedExceptionCount = 0; @@ -531,16 +531,16 @@ namespace osu.Game.Tests.Database Interlocked.Increment(ref loggedExceptionCount); }; - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); - realmFactory.Context.Write(() => imported.Hash += "-changed"); + realm.Realm.Write(() => imported.Hash += "-changed"); - checkBeatmapSetCount(realmFactory.Context, 1); - checkBeatmapCount(realmFactory.Context, 12); - checkSingleReferencedFileCount(realmFactory.Context, 18); + checkBeatmapSetCount(realm.Realm, 1); + checkBeatmapCount(realm.Realm, 12); + checkSingleReferencedFileCount(realm.Realm, 18); string? brokenTempFilename = TestResources.GetTestBeatmapForImport(); @@ -565,10 +565,10 @@ namespace osu.Game.Tests.Database { } - checkBeatmapSetCount(realmFactory.Context, 1); - checkBeatmapCount(realmFactory.Context, 12); + checkBeatmapSetCount(realm.Realm, 1); + checkBeatmapCount(realm.Realm, 12); - checkSingleReferencedFileCount(realmFactory.Context, 18); + checkSingleReferencedFileCount(realm.Realm, 18); Assert.AreEqual(1, loggedExceptionCount); @@ -579,18 +579,18 @@ namespace osu.Game.Tests.Database [Test] public void TestImportThenDeleteThenImportOptimisedPath() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); - deleteBeatmapSet(imported, realmFactory.Context); + deleteBeatmapSet(imported, realm.Realm); Assert.IsTrue(imported.DeletePending); - var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID == importedSecondTime.ID); @@ -603,18 +603,18 @@ namespace osu.Game.Tests.Database [Test] public void TestImportThenDeleteThenImportNonOptimisedPath() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new NonOptimisedBeatmapImporter(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new NonOptimisedBeatmapImporter(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); - deleteBeatmapSet(imported, realmFactory.Context); + deleteBeatmapSet(imported, realm.Realm); Assert.IsTrue(imported.DeletePending); - var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID == importedSecondTime.ID); @@ -627,22 +627,22 @@ namespace osu.Game.Tests.Database [Test] public void TestImportThenDeleteThenImportWithOnlineIDsMissing() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); - realmFactory.Context.Write(() => + realm.Realm.Write(() => { foreach (var b in imported.Beatmaps) b.OnlineID = -1; }); - deleteBeatmapSet(imported, realmFactory.Context); + deleteBeatmapSet(imported, realm.Realm); - var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched) Assert.IsTrue(imported.ID != importedSecondTime.ID); @@ -653,10 +653,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportWithDuplicateBeatmapIDs() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); var metadata = new BeatmapMetadata { @@ -667,7 +667,7 @@ namespace osu.Game.Tests.Database } }; - var ruleset = realmFactory.Context.All().First(); + var ruleset = realm.Realm.All().First(); var toImport = new BeatmapSetInfo { @@ -699,15 +699,15 @@ namespace osu.Game.Tests.Database [Test] public void TestImportWhenFileOpen() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); using (File.OpenRead(temp)) await importer.Import(temp); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); File.Delete(temp); Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); }); @@ -716,10 +716,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportWithDuplicateHashes() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -740,7 +740,7 @@ namespace osu.Game.Tests.Database await importer.Import(temp); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); } finally { @@ -752,10 +752,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportNestedStructure() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -780,7 +780,7 @@ namespace osu.Game.Tests.Database Assert.NotNull(imported); Debug.Assert(imported != null); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("subfolder"))), "Files contain common subfolder"); } @@ -794,10 +794,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportWithIgnoredDirectoryInArchive() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -830,7 +830,7 @@ namespace osu.Game.Tests.Database Assert.NotNull(imported); Debug.Assert(imported != null); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("__MACOSX"))), "Files contain resource fork folder, which should be ignored"); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("actual_data"))), "Files contain common subfolder"); @@ -845,22 +845,22 @@ namespace osu.Game.Tests.Database [Test] public void TestUpdateBeatmapInfo() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); await importer.Import(temp); // Update via the beatmap, not the beatmap info, to ensure correct linking - BeatmapSetInfo setToUpdate = realmFactory.Context.All().First(); + BeatmapSetInfo setToUpdate = realm.Realm.All().First(); var beatmapToUpdate = setToUpdate.Beatmaps.First(); - realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated"); + realm.Realm.Write(() => beatmapToUpdate.DifficultyName = "updated"); - BeatmapInfo updatedInfo = realmFactory.Context.All().First(b => b.ID == beatmapToUpdate.ID); + BeatmapInfo updatedInfo = realm.Realm.All().First(b => b.ID == beatmapToUpdate.ID); Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated")); }); } @@ -1004,8 +1004,8 @@ namespace osu.Game.Tests.Database public class NonOptimisedBeatmapImporter : BeatmapImporter { - public NonOptimisedBeatmapImporter(RealmContextFactory realmFactory, Storage storage) - : base(realmFactory, storage) + public NonOptimisedBeatmapImporter(RealmAccess realm, Storage storage) + : base(realm, storage) { } diff --git a/osu.Game.Tests/Database/FileStoreTests.cs b/osu.Game.Tests/Database/FileStoreTests.cs index 3cb4705381..008aa3d833 100644 --- a/osu.Game.Tests/Database/FileStoreTests.cs +++ b/osu.Game.Tests/Database/FileStoreTests.cs @@ -19,10 +19,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportFile() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realmAccess, storage) => { - var realm = realmFactory.Context; - var files = new RealmFileStore(realmFactory, storage); + var realm = realmAccess.Context; + var files = new RealmFileStore(realmAccess, storage); var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); @@ -36,10 +36,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportSameFileTwice() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realmAccess, storage) => { - var realm = realmFactory.Context; - var files = new RealmFileStore(realmFactory, storage); + var realm = realmAccess.Context; + var files = new RealmFileStore(realmAccess, storage); var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); @@ -53,10 +53,10 @@ namespace osu.Game.Tests.Database [Test] public void TestDontPurgeReferenced() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realmAccess, storage) => { - var realm = realmFactory.Context; - var files = new RealmFileStore(realmFactory, storage); + var realm = realmAccess.Context; + var files = new RealmFileStore(realmAccess, storage); var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); @@ -92,10 +92,10 @@ namespace osu.Game.Tests.Database [Test] public void TestPurgeUnreferenced() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realmAccess, storage) => { - var realm = realmFactory.Context; - var files = new RealmFileStore(realmFactory, storage); + var realm = realmAccess.Context; + var files = new RealmFileStore(realmAccess, storage); var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 3c62153d9e..2533c832e6 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -21,15 +21,15 @@ namespace osu.Game.Tests.Database [Test] public void TestConstructRealm() { - RunTestWithRealm((realmFactory, _) => { realmFactory.Run(realm => realm.Refresh()); }); + RunTestWithRealm((realm, _) => { realm.Run(r => r.Refresh()); }); } [Test] public void TestBlockOperations() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) { } }); @@ -42,22 +42,22 @@ namespace osu.Game.Tests.Database [Test] public void TestNestedContextCreationWithSubscription() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { bool callbackRan = false; - realmFactory.RegisterCustomSubscription(realm => + realm.RegisterCustomSubscription(r => { - var subscription = realm.All().QueryAsyncWithNotifications((sender, changes, error) => + var subscription = r.All().QueryAsyncWithNotifications((sender, changes, error) => { - realmFactory.Run(_ => + realm.Run(_ => { callbackRan = true; }); }); // Force the callback above to run. - realmFactory.Run(r => r.Refresh()); + realm.Run(rr => rr.Refresh()); subscription?.Dispose(); return null; @@ -70,14 +70,14 @@ namespace osu.Game.Tests.Database [Test] public void TestBlockOperationsWithContention() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { ManualResetEventSlim stopThreadedUsage = new ManualResetEventSlim(); ManualResetEventSlim hasThreadedUsage = new ManualResetEventSlim(); Task.Factory.StartNew(() => { - realmFactory.Run(_ => + realm.Run(_ => { hasThreadedUsage.Set(); @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Database Assert.Throws(() => { - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) { } }); diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index d53fcb9ac7..2e3f708f79 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -21,11 +21,11 @@ namespace osu.Game.Tests.Database [Test] public void TestLiveEquality() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { - ILive beatmap = realmFactory.Run(realm => realm.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory)); + ILive beatmap = realm.Run(r => r.Write(_ => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realm)); - ILive beatmap2 = realmFactory.Run(realm => realm.All().First().ToLive(realmFactory)); + ILive beatmap2 = realm.Run(r => r.All().First().ToLive(realm)); Assert.AreEqual(beatmap, beatmap2); }); @@ -34,20 +34,20 @@ namespace osu.Game.Tests.Database [Test] public void TestAccessAfterStorageMigrate() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realm, storage) => { var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); ILive? liveBeatmap = null; - realmFactory.Run(realm => + realm.Run(r => { - realm.Write(r => r.Add(beatmap)); + r.Write(_ => r.Add(beatmap)); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) { // recycle realm before migrating } @@ -66,13 +66,13 @@ namespace osu.Game.Tests.Database [Test] public void TestAccessAfterAttach() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(realmFactory); + var liveBeatmap = beatmap.ToLive(realm); - realmFactory.Run(realm => realm.Write(r => r.Add(beatmap))); + realm.Run(r => r.Write(_ => r.Add(beatmap))); Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); }); @@ -98,16 +98,16 @@ namespace osu.Game.Tests.Database [Test] public void TestScopedReadWithoutContext() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - realmFactory.Run(threadContext => + realm.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); @@ -127,16 +127,16 @@ namespace osu.Game.Tests.Database [Test] public void TestScopedWriteWithoutContext() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - realmFactory.Run(threadContext => + realm.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); @@ -153,10 +153,10 @@ namespace osu.Game.Tests.Database [Test] public void TestValueAccessNonManaged() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(realmFactory); + var liveBeatmap = beatmap.ToLive(realm); Assert.DoesNotThrow(() => { @@ -168,17 +168,17 @@ namespace osu.Game.Tests.Database [Test] public void TestValueAccessWithOpenContextFails() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - realmFactory.Run(threadContext => + realm.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Database }); // Can't be used, even from within a valid context. - realmFactory.Run(threadContext => + realm.Run(threadContext => { Assert.Throws(() => { @@ -207,16 +207,16 @@ namespace osu.Game.Tests.Database [Test] public void TestValueAccessWithoutOpenContextFails() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - realmFactory.Run(threadContext => + realm.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); @@ -235,18 +235,18 @@ namespace osu.Game.Tests.Database [Test] public void TestLiveAssumptions() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { int changesTriggered = 0; - realmFactory.RegisterCustomSubscription(outerRealm => + realm.RegisterCustomSubscription(outerRealm => { outerRealm.All().QueryAsyncWithNotifications(gotChange); ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - realmFactory.Run(innerRealm => + realm.Run(innerRealm => { var ruleset = CreateRuleset(); var beatmap = innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); @@ -255,7 +255,7 @@ namespace osu.Game.Tests.Database // not just a refresh from the resolved Live. innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 1799b95905..d62ce3b585 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -24,11 +24,11 @@ namespace osu.Game.Tests.Database IEnumerable? resolvedItems = null; ChangeSet? lastChanges = null; - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { - realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); - var registration = realmFactory.RegisterForNotifications(realm => realm.All(), onChanged); + var registration = realm.RegisterForNotifications(r => r.All(), onChanged); testEventsArriving(true); @@ -37,10 +37,10 @@ namespace osu.Game.Tests.Database resolvedItems = null; lastChanges = null; - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) Assert.That(resolvedItems, Is.Empty); - realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); testEventsArriving(true); @@ -50,34 +50,34 @@ namespace osu.Game.Tests.Database registration.Dispose(); - realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); testEventsArriving(false); // And make sure even after another context loss we don't get firings. - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) Assert.That(resolvedItems, Is.Null); - realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); testEventsArriving(false); void testEventsArriving(bool shouldArrive) { - realmFactory.Run(realm => realm.Refresh()); + realm.Run(r => r.Refresh()); if (shouldArrive) Assert.That(resolvedItems, Has.One.Items); else Assert.That(resolvedItems, Is.Null); - realmFactory.Write(realm => + realm.Write(r => { - realm.RemoveAll(); - realm.RemoveAll(); + r.RemoveAll(); + r.RemoveAll(); }); - realmFactory.Run(realm => realm.Refresh()); + realm.Run(r => r.Refresh()); if (shouldArrive) Assert.That(lastChanges?.DeletedIndices, Has.One.Items); @@ -98,39 +98,39 @@ namespace osu.Game.Tests.Database [Test] public void TestCustomRegisterWithContextLoss() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { BeatmapSetInfo? beatmapSetInfo = null; - realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo())); + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); - var subscription = realmFactory.RegisterCustomSubscription(realm => + var subscription = realm.RegisterCustomSubscription(r => { - beatmapSetInfo = realm.All().First(); + beatmapSetInfo = r.All().First(); return new InvokeOnDisposal(() => beatmapSetInfo = null); }); Assert.That(beatmapSetInfo, Is.Not.Null); - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) { // custom disposal action fired when context lost. Assert.That(beatmapSetInfo, Is.Null); } // re-registration after context restore. - realmFactory.Run(realm => realm.Refresh()); + realm.Run(r => r.Refresh()); Assert.That(beatmapSetInfo, Is.Not.Null); subscription.Dispose(); Assert.That(beatmapSetInfo, Is.Null); - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) Assert.That(beatmapSetInfo, Is.Null); - realmFactory.Run(realm => realm.Refresh()); + realm.Run(r => r.Refresh()); Assert.That(beatmapSetInfo, Is.Null); }); } diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 0cee165f75..c2339dd9ad 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Database storage.DeleteDirectory(string.Empty); } - protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") + protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") { using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller)) { @@ -39,22 +39,22 @@ namespace osu.Game.Tests.Database // ReSharper disable once AccessToDisposedClosure var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller)); - using (var realmFactory = new RealmContextFactory(testStorage, "client")) + using (var realm = new RealmAccess(testStorage, "client")) { - Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); - testAction(realmFactory, testStorage); + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}"); + testAction(realm, testStorage); - realmFactory.Dispose(); + realm.Dispose(); - Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); - realmFactory.Compact(); - Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); + Logger.Log($"Final database size: {getFileSize(testStorage, realm)}"); + realm.Compact(); + Logger.Log($"Final database size after compact: {getFileSize(testStorage, realm)}"); } })); } } - protected void RunTestWithRealmAsync(Func testAction, [CallerMemberName] string caller = "") + protected void RunTestWithRealmAsync(Func testAction, [CallerMemberName] string caller = "") { using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller)) { @@ -62,15 +62,15 @@ namespace osu.Game.Tests.Database { var testStorage = storage.GetStorageForDirectory(caller); - using (var realmFactory = new RealmContextFactory(testStorage, "client")) + using (var realm = new RealmAccess(testStorage, "client")) { - Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); - await testAction(realmFactory, testStorage); + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}"); + await testAction(realm, testStorage); - realmFactory.Dispose(); + realm.Dispose(); - Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); - realmFactory.Compact(); + Logger.Log($"Final database size: {getFileSize(testStorage, realm)}"); + realm.Compact(); } })); } @@ -138,11 +138,11 @@ namespace osu.Game.Tests.Database } } - private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory) + private static long getFileSize(Storage testStorage, RealmAccess realm) { try { - using (var stream = testStorage.GetStream(realmFactory.Filename)) + using (var stream = testStorage.GetStream(realm.Filename)) return stream?.Length ?? 0; } catch diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index 4416da6f92..7544142b70 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -12,37 +12,37 @@ namespace osu.Game.Tests.Database [Test] public void TestCreateStore() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realm, storage) => { - var rulesets = new RulesetStore(realmFactory, storage); + var rulesets = new RulesetStore(realm, storage); Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); - Assert.AreEqual(4, realmFactory.Context.All().Count()); + Assert.AreEqual(4, realm.Realm.All().Count()); }); } [Test] public void TestCreateStoreTwiceDoesntAddRulesetsAgain() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realm, storage) => { - var rulesets = new RulesetStore(realmFactory, storage); - var rulesets2 = new RulesetStore(realmFactory, storage); + var rulesets = new RulesetStore(realm, storage); + var rulesets2 = new RulesetStore(realm, storage); Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); Assert.AreEqual(4, rulesets2.AvailableRulesets.Count()); Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First()); - Assert.AreEqual(4, realmFactory.Context.All().Count()); + Assert.AreEqual(4, realm.Realm.All().Count()); }); } [Test] public void TestRetrievedRulesetsAreDetached() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realm, storage) => { - var rulesets = new RulesetStore(realmFactory, storage); + var rulesets = new RulesetStore(realm, storage); Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged); Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged); diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index c1041e9fd6..4b8816f142 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Database private RealmKeyBindingStore keyBindingStore; - private RealmContextFactory realmContextFactory; + private RealmAccess realm; [SetUp] public void SetUp() @@ -33,8 +33,8 @@ namespace osu.Game.Tests.Database storage = new NativeStorage(directory.FullName); - realmContextFactory = new RealmContextFactory(storage, "test"); - keyBindingStore = new RealmKeyBindingStore(realmContextFactory, new ReadableKeyCombinationProvider()); + realm = new RealmAccess(storage, "test"); + keyBindingStore = new RealmKeyBindingStore(realm, new ReadableKeyCombinationProvider()); } [Test] @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Database KeyBindingContainer testContainer = new TestKeyBindingContainer(); // Add some excess bindings for an action which only supports 1. - realmContextFactory.Write(realm => + realm.Write(realm => { realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.A))); realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.S))); @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Database private int queryCount(GlobalAction? match = null) { - return realmContextFactory.Run(realm => + return realm.Run(realm => { var results = realm.All(); if (match.HasValue) @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Database keyBindingStore.Register(testContainer, Enumerable.Empty()); - realmContextFactory.Run(outerRealm => + realm.Run(outerRealm => { var backBinding = outerRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); @@ -100,7 +100,7 @@ namespace osu.Game.Tests.Database var tsr = ThreadSafeReference.Create(backBinding); - realmContextFactory.Run(innerRealm => + realm.Run(innerRealm => { var binding = innerRealm.ResolveReference(tsr); innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Database [TearDown] public void TearDown() { - realmContextFactory.Dispose(); + realm.Dispose(); storage.DeleteDirectory(string.Empty); } diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index f0ebd7a8cc..88862ea28b 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -261,7 +261,7 @@ namespace osu.Game.Tests.Gameplay public AudioManager AudioManager => Audio; public IResourceStore Files => null; public new IResourceStore Resources => base.Resources; - public RealmContextFactory RealmContextFactory => null; + public RealmAccess RealmAccess => null; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; #endregion diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 1d639c6418..e35e4d9b15 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -45,8 +45,8 @@ namespace osu.Game.Tests.Online [BackgroundDependencyLoader] private void load(AudioManager audio, GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.CacheAs(beatmaps = new TestBeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.CacheAs(beatmaps = new TestBeatmapManager(LocalStorage, Access, rulesets, API, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API, host)); } @@ -60,8 +60,8 @@ namespace osu.Game.Tests.Online testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile); testBeatmapSet = testBeatmapInfo.BeatmapSet; - ContextFactory.Write(r => r.RemoveAll()); - ContextFactory.Write(r => r.RemoveAll()); + Access.Write(r => r.RemoveAll()); + Access.Write(r => r.RemoveAll()); selectedItem.Value = new PlaylistItem { @@ -166,22 +166,22 @@ namespace osu.Game.Tests.Online public Task> CurrentImportTask { get; private set; } - public TestBeatmapManager(Storage storage, RealmContextFactory 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 TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) + : base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap) { } - protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) { - return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, onlineLookupQueue); + return new TestBeatmapModelManager(this, storage, realm, rulesets, onlineLookupQueue); } internal class TestBeatmapModelManager : BeatmapModelManager { private readonly TestBeatmapManager testBeatmapManager; - public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmContextFactory databaseContextFactory, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) - : base(databaseContextFactory, storage, beatmapOnlineLookupQueue) + public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) + : base(databaseAccess, storage, beatmapOnlineLookupQueue) { this.testBeatmapManager = testBeatmapManager; } diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 4ab4c08353..fbfa7eda6a 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -47,10 +47,10 @@ namespace osu.Game.Tests.Visual.Background [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(Access); manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 18572ac211..fd0645a1e9 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.Collections [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Access, rulesets, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index d4282ff21e..07b2bdcba3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -47,9 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 99c867b014..0bf1d60ac5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -43,9 +43,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 373b165acc..063d886729 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -61,9 +61,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 15ebe0ee00..8c79c468d7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -42,9 +42,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); beatmaps = new List(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 012a2fd960..77e2c9c714 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -38,9 +38,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index d547b42891..4270818b1a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -33,9 +33,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); } [SetUp] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 965b142ed7..56e64292c6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -38,9 +38,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 1c346e09d5..afb60b62aa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -40,9 +40,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 221732910b..79b29f0eca 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -41,9 +41,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 0b0006e437..6ed57e9899 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -34,9 +34,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); var beatmapSet = TestResources.CreateTestBeatmapSetInfo(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 39cde0ad87..bd95b297d4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -42,9 +42,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index 0f314242b4..7cb29895eb 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Navigation .ChildrenOfType().SingleOrDefault(); private RealmKeyBinding firstOsuRulesetKeyBindings => Game.Dependencies - .Get().Context + .Get().Context .All() .AsEnumerable() .First(k => k.RulesetName == "osu" && k.ActionInt == 0); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index bc9f759bdd..716e3a535d 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -40,9 +40,9 @@ namespace osu.Game.Tests.Visual.Playlists [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); } [SetUpSteps] diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index a77480ee54..13b4af5223 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -36,13 +36,13 @@ namespace osu.Game.Tests.Visual.Ranking private BeatmapManager beatmaps { get; set; } [Resolved] - private RealmContextFactory realmContextFactory { get; set; } + private RealmAccess realm { get; set; } protected override void LoadComplete() { base.LoadComplete(); - realmContextFactory.Run(realm => + realm.Run(realm => { var beatmapInfo = realm.All() .Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 2e1a66be5f..2f5594379b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -42,10 +42,10 @@ namespace osu.Game.Tests.Visual.SongSelect { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); - dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, ContextFactory, Scheduler)); - Dependencies.Cache(ContextFactory); + dependencies.Cache(rulesetStore = new RulesetStore(Access)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Access, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Access, Scheduler)); + Dependencies.Cache(Access); return dependencies; } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index ca8e9d2eff..4868a4a075 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Access, rulesets, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Access); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 6295a52bdd..d8a39dda01 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -47,9 +47,9 @@ namespace osu.Game.Tests.Visual.SongSelect { // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. // At a point we have isolated interactive test runs enough, this can likely be removed. - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(ContextFactory); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(Access); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); Dependencies.Cache(music = new MusicController()); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs index 3aa5a759e6..a0657ffdf6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs @@ -28,10 +28,10 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, ContextFactory, Scheduler)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Access)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Access, Scheduler)); + Dependencies.Cache(Access); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index f43354514b..2278d3f8bf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.UserInterface private BeatmapInfo beatmapInfo; [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } [Cached] private readonly DialogOverlay dialogOverlay; @@ -87,10 +87,10 @@ namespace osu.Game.Tests.Visual.UserInterface { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); - dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, ContextFactory, Scheduler)); - Dependencies.Cache(ContextFactory); + dependencies.Cache(rulesetStore = new RulesetStore(Access)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Access, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Access, Scheduler)); + Dependencies.Cache(Access); var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.UserInterface [SetUp] public void Setup() => Schedule(() => { - realmFactory.Run(realm => + realm.Run(realm => { // Due to soft deletions, we can re-use deleted scores between test runs scoreManager.Undelete(realm.All().Where(s => s.DeletePending).ToList()); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 43e4b482bd..ddc1d054cc 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -41,11 +41,11 @@ namespace osu.Game.Beatmaps private readonly WorkingBeatmapCache workingBeatmapCache; private readonly BeatmapOnlineLookupQueue? onlineBeatmapLookupQueue; - private readonly RealmContextFactory contextFactory; + private readonly RealmAccess realm; - public BeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore gameResources, GameHost? host = null, WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false) + public BeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore gameResources, GameHost? host = null, WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false) { - this.contextFactory = contextFactory; + this.realm = realm; if (performOnlineLookups) { @@ -55,11 +55,11 @@ namespace osu.Game.Beatmaps onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); } - var userResources = new RealmFileStore(contextFactory, storage).Store; + var userResources = new RealmFileStore(realm, storage).Store; BeatmapTrackStore = audioManager.GetTrackStore(userResources); - beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, onlineBeatmapLookupQueue); + beatmapModelManager = CreateBeatmapModelManager(storage, realm, rulesets, onlineBeatmapLookupQueue); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache; @@ -70,8 +70,8 @@ namespace osu.Game.Beatmaps return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host); } - protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue? onlineLookupQueue) => - new BeatmapModelManager(contextFactory, storage, onlineLookupQueue); + protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue? onlineLookupQueue) => + new BeatmapModelManager(realm, storage, onlineLookupQueue); /// /// Create a new . @@ -119,7 +119,7 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to hide. public void Hide(BeatmapInfo beatmapInfo) { - contextFactory.Run(realm => + realm.Run(realm => { using (var transaction = realm.BeginWrite()) { @@ -138,7 +138,7 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to restore. public void Restore(BeatmapInfo beatmapInfo) { - contextFactory.Run(realm => + realm.Run(realm => { using (var transaction = realm.BeginWrite()) { @@ -153,7 +153,7 @@ namespace osu.Game.Beatmaps public void RestoreAll() { - contextFactory.Run(realm => + realm.Run(realm => { using (var transaction = realm.BeginWrite()) { @@ -171,7 +171,7 @@ namespace osu.Game.Beatmaps /// A list of available . public List GetAllUsableBeatmapSets() { - return contextFactory.Run(realm => + return realm.Run(realm => { realm.Refresh(); return realm.All().Where(b => !b.DeletePending).Detach(); @@ -185,7 +185,7 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public ILive? QueryBeatmapSet(Expression> query) { - return contextFactory.Run(realm => realm.All().FirstOrDefault(query)?.ToLive(contextFactory)); + return realm.Run(r => r.All().FirstOrDefault(query)?.ToLive(realm)); } #region Delegation to BeatmapModelManager (methods which previously existed locally). @@ -240,7 +240,7 @@ namespace osu.Game.Beatmaps public void Delete(Expression>? filter = null, bool silent = false) { - contextFactory.Run(realm => + realm.Run(realm => { var items = realm.All().Where(s => !s.DeletePending && !s.Protected); @@ -253,7 +253,7 @@ namespace osu.Game.Beatmaps public void UndeleteAll() { - contextFactory.Run(realm => beatmapModelManager.Undelete(realm.All().Where(s => s.DeletePending).ToList())); + realm.Run(realm => beatmapModelManager.Undelete(realm.All().Where(s => s.DeletePending).ToList())); } public void Undelete(List items, bool silent = false) @@ -312,7 +312,7 @@ namespace osu.Game.Beatmaps // If we seem to be missing files, now is a good time to re-fetch. if (importedBeatmap?.BeatmapSet?.Files.Count == 0) { - contextFactory.Run(realm => + realm.Run(realm => { var refetch = realm.Find(importedBeatmap.ID)?.Detach(); diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index ead86c1059..167d77d6f6 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -33,8 +33,8 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; - public BeatmapModelManager(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) - : base(contextFactory, storage, onlineLookupQueue) + public BeatmapModelManager(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) + : base(realm, storage, onlineLookupQueue) { } @@ -98,12 +98,12 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapInfo? QueryBeatmap(Expression> query) { - return ContextFactory.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); + return Access.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); } public void Update(BeatmapSetInfo item) { - ContextFactory.Write(realm => + Access.Write(realm => { var existing = realm.Find(item.ID); item.CopyChangesToRealm(existing); diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 6947752c47..d3f356bb24 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -100,7 +100,7 @@ namespace osu.Game.Beatmaps TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore; AudioManager IStorageResourceProvider.AudioManager => audioManager; - RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; + RealmAccess IStorageResourceProvider.RealmAccess => null; IResourceStore IStorageResourceProvider.Files => files; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs index 2bba20fb09..e5d2d572c8 100644 --- a/osu.Game/Configuration/SettingsStore.cs +++ b/osu.Game/Configuration/SettingsStore.cs @@ -10,11 +10,11 @@ namespace osu.Game.Configuration // this class mostly exists as a wrapper to avoid breaking the ruleset API (see usage in RulesetConfigManager). // it may cease to exist going forward, depending on how the structure of the config data layer changes. - public readonly RealmContextFactory Realm; + public readonly RealmAccess Realm; - public SettingsStore(RealmContextFactory realmFactory) + public SettingsStore(RealmAccess realm) { - Realm = realmFactory; + Realm = realm; } } } diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index edce99e302..a0787e81e6 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -35,7 +35,7 @@ namespace osu.Game.Database private DatabaseContextFactory efContextFactory { get; set; } = null!; [Resolved] - private RealmContextFactory realmContextFactory { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -101,7 +101,7 @@ namespace osu.Game.Database { using (var ef = efContextFactory.Get()) { - realmContextFactory.Write(realm => + realm.Write(realm => { // Before beginning, ensure realm is in an empty state. // Migrations which are half-completed could lead to issues if the user tries a second time. @@ -158,7 +158,7 @@ namespace osu.Game.Database int count = existingBeatmapSets.Count(); - realmContextFactory.Run(realm => + realm.Run(realm => { log($"Found {count} beatmaps in EF"); @@ -280,7 +280,7 @@ namespace osu.Game.Database int count = existingScores.Count(); - realmContextFactory.Run(realm => + realm.Run(realm => { log($"Found {count} scores in EF"); @@ -369,7 +369,7 @@ namespace osu.Game.Database break; } - realmContextFactory.Run(realm => + realm.Run(realm => { using (var transaction = realm.BeginWrite()) { @@ -428,7 +428,7 @@ namespace osu.Game.Database log("Beginning settings migration to realm"); - realmContextFactory.Run(realm => + realm.Run(realm => { using (var transaction = realm.BeginWrite()) { diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmAccess.cs similarity index 98% rename from osu.Game/Database/RealmContextFactory.cs rename to osu.Game/Database/RealmAccess.cs index 778092c543..2f397639c9 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -32,7 +32,7 @@ namespace osu.Game.Database /// /// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage. /// - public class RealmContextFactory : IDisposable + public class RealmAccess : IDisposable { private readonly Storage storage; @@ -123,7 +123,7 @@ namespace osu.Game.Database /// The game storage which will be used to create the realm backing file. /// The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified. /// An EF factory used only for migration purposes. - public RealmContextFactory(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null) + public RealmAccess(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null) { this.storage = storage; this.efContextFactory = efContextFactory; @@ -365,7 +365,7 @@ namespace osu.Game.Database private Realm createContext() { if (isDisposed) - throw new ObjectDisposedException(nameof(RealmContextFactory)); + throw new ObjectDisposedException(nameof(RealmAccess)); bool tookSemaphoreLock = false; @@ -592,7 +592,7 @@ namespace osu.Game.Database public IDisposable BlockAllOperations() { if (isDisposed) - throw new ObjectDisposedException(nameof(RealmContextFactory)); + throw new ObjectDisposedException(nameof(RealmAccess)); SynchronizationContext? syncContext = null; @@ -652,7 +652,7 @@ namespace osu.Game.Database throw; } - return new InvokeOnDisposal(this, factory => + return new InvokeOnDisposal(this, factory => { factory.contextCreationLock.Release(); Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index df5e165f8e..29159fd5be 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -24,17 +24,17 @@ namespace osu.Game.Database /// private readonly T data; - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realm; /// /// Construct a new instance of live realm data. /// /// The realm data. - /// The realm factory the data was sourced from. May be null for an unmanaged object. - public RealmLive(T data, RealmContextFactory realmFactory) + /// The realm factory the data was sourced from. May be null for an unmanaged object. + public RealmLive(T data, RealmAccess realm) { this.data = data; - this.realmFactory = realmFactory; + this.realm = realm; ID = data.ID; } @@ -51,7 +51,7 @@ namespace osu.Game.Database return; } - realmFactory.Run(realm => + realm.Run(realm => { perform(retrieveFromID(realm, ID)); }); @@ -66,7 +66,7 @@ namespace osu.Game.Database if (!IsManaged) return perform(data); - return realmFactory.Run(realm => + return realm.Run(realm => { var returnData = perform(retrieveFromID(realm, ID)); @@ -104,7 +104,7 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads"); - return realmFactory.Context.Find(ID); + return realm.Realm.Find(ID); } } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index d9026d165d..d4f8978ac5 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -216,16 +216,16 @@ namespace osu.Game.Database return new RealmLiveUnmanaged(realmObject); } - public static List> ToLive(this IEnumerable realmList, RealmContextFactory realmContextFactory) + public static List> ToLive(this IEnumerable realmList, RealmAccess realm) where T : RealmObject, IHasGuidPrimaryKey { - return realmList.Select(l => new RealmLive(l, realmContextFactory)).Cast>().ToList(); + return realmList.Select(l => new RealmLive(l, realm)).Cast>().ToList(); } - public static ILive ToLive(this T realmObject, RealmContextFactory realmContextFactory) + public static ILive ToLive(this T realmObject, RealmAccess realm) where T : RealmObject, IHasGuidPrimaryKey { - return new RealmLive(realmObject, realmContextFactory); + return new RealmLive(realmObject, realm); } /// @@ -271,8 +271,8 @@ namespace osu.Game.Database public static IDisposable? QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) where T : RealmObjectBase { - if (!RealmContextFactory.CurrentThreadSubscriptionsAllowed) - throw new InvalidOperationException($"Make sure to call {nameof(RealmContextFactory)}.{nameof(RealmContextFactory.RegisterForNotifications)}"); + if (!RealmAccess.CurrentThreadSubscriptionsAllowed) + throw new InvalidOperationException($"Make sure to call {nameof(RealmAccess)}.{nameof(RealmAccess.RegisterForNotifications)}"); return collection.SubscribeForNotifications(callback); } diff --git a/osu.Game/IO/IStorageResourceProvider.cs b/osu.Game/IO/IStorageResourceProvider.cs index 950b5aae09..b381ac70b0 100644 --- a/osu.Game/IO/IStorageResourceProvider.cs +++ b/osu.Game/IO/IStorageResourceProvider.cs @@ -28,7 +28,7 @@ namespace osu.Game.IO /// /// Access realm. /// - RealmContextFactory RealmContextFactory { get; } + RealmAccess RealmAccess { get; } /// /// Create a texture loader store based on an underlying data store. diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 9cd9865441..d54c049c99 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -25,7 +25,7 @@ namespace osu.Game.Input.Bindings private IDisposable realmSubscription; [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); @@ -49,13 +49,13 @@ namespace osu.Game.Input.Bindings private IQueryable queryRealmKeyBindings() { string rulesetName = ruleset?.ShortName; - return realmFactory.Context.All() - .Where(b => b.RulesetName == rulesetName && b.Variant == variant); + return realm.Realm.All() + .Where(b => b.RulesetName == rulesetName && b.Variant == variant); } protected override void LoadComplete() { - realmSubscription = realmFactory.RegisterForNotifications(realm => queryRealmKeyBindings(), (sender, changes, error) => + realmSubscription = realm.RegisterForNotifications(realm => queryRealmKeyBindings(), (sender, changes, error) => { // The first fire of this is a bit redundant as this is being called in base.LoadComplete, // but this is safest in case the subscription is restored after a context recycle. diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 60f7eb2198..cccd42a9aa 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -16,12 +16,12 @@ namespace osu.Game.Input { public class RealmKeyBindingStore { - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realm; private readonly ReadableKeyCombinationProvider keyCombinationProvider; - public RealmKeyBindingStore(RealmContextFactory realmFactory, ReadableKeyCombinationProvider keyCombinationProvider) + public RealmKeyBindingStore(RealmAccess realm, ReadableKeyCombinationProvider keyCombinationProvider) { - this.realmFactory = realmFactory; + this.realm = realm; this.keyCombinationProvider = keyCombinationProvider; } @@ -34,7 +34,7 @@ namespace osu.Game.Input { List combinations = new List(); - realmFactory.Run(context => + realm.Run(context => { foreach (var action in context.All().Where(b => string.IsNullOrEmpty(b.RulesetName) && (GlobalAction)b.ActionInt == globalAction)) { @@ -56,7 +56,7 @@ namespace osu.Game.Input /// The rulesets to populate defaults from. public void Register(KeyBindingContainer container, IEnumerable rulesets) { - realmFactory.Run(realm => + realm.Run(realm => { using (var transaction = realm.BeginWrite()) { diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 3f45afb6b2..f54dc30620 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -22,7 +22,7 @@ namespace osu.Game.Online private IDisposable? realmSubscription; [Resolved] - private RealmContextFactory realmContextFactory { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; public BeatmapDownloadTracker(IBeatmapSetInfo trackedItem) : base(trackedItem) @@ -42,7 +42,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realmContextFactory.RegisterForNotifications(realm => realm.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(realm => realm.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 28c41bec33..c562695ac1 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -30,7 +30,7 @@ namespace osu.Game.Online.Rooms protected override bool RequiresChildrenUpdate => true; [Resolved] - private RealmContextFactory realmContextFactory { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; /// /// The availability state of the currently selected playlist item. @@ -78,7 +78,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realmContextFactory.RegisterForNotifications(realm => filteredBeatmaps(), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(realm => filteredBeatmaps(), (items, changes, ___) => { if (changes == null) return; @@ -128,9 +128,9 @@ namespace osu.Game.Online.Rooms int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID; string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; - return realmContextFactory.Context - .All() - .Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum); + return realm.Realm + .All() + .Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index 64ceee9fe6..81dfc811a4 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -23,7 +23,7 @@ namespace osu.Game.Online private IDisposable? realmSubscription; [Resolved] - private RealmContextFactory realmContextFactory { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; public ScoreDownloadTracker(ScoreInfo trackedItem) : base(trackedItem) @@ -47,7 +47,7 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; - realmSubscription = realmContextFactory.RegisterForNotifications(realm => realm.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(realm => realm.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending), (items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 710a7be8d4..7a6a126900 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -149,7 +149,7 @@ namespace osu.Game private MultiplayerClient multiplayerClient; - private RealmContextFactory realmFactory; + private RealmAccess realm; protected override Container Content => content; @@ -192,9 +192,9 @@ namespace osu.Game if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME)) dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage)); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", EFContextFactory)); + dependencies.Cache(realm = new RealmAccess(Storage, "client", EFContextFactory)); - dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage)); + dependencies.Cache(RulesetStore = new RulesetStore(realm, Storage)); dependencies.CacheAs(RulesetStore); // Backup is taken here rather than in EFToRealmMigrator to avoid recycling realm contexts @@ -205,7 +205,7 @@ namespace osu.Game string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"; EFContextFactory.CreateBackup($"client.{migration}.db"); - realmFactory.CreateBackup($"client.{migration}.realm"); + realm.CreateBackup($"client.{migration}.realm"); using (var source = Storage.GetStream("collection.db")) using (var destination = Storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew)) @@ -225,7 +225,7 @@ namespace osu.Game Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; - dependencies.Cache(SkinManager = new SkinManager(Storage, realmFactory, Host, Resources, Audio, Scheduler)); + dependencies.Cache(SkinManager = new SkinManager(Storage, realm, Host, Resources, Audio, Scheduler)); dependencies.CacheAs(SkinManager); EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); @@ -240,8 +240,8 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() - dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realmFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realmFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, Host, () => difficultyCache, LocalConfig)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API)); dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API)); @@ -259,7 +259,7 @@ namespace osu.Game dependencies.Cache(scorePerformanceManager); AddInternal(scorePerformanceManager); - dependencies.CacheAs(rulesetConfigCache = new RulesetConfigCache(realmFactory, RulesetStore)); + dependencies.CacheAs(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore)); var powerStatus = CreateBatteryInfo(); if (powerStatus != null) @@ -303,7 +303,7 @@ namespace osu.Game base.Content.Add(CreateScalingContainer().WithChildren(mainContent)); - KeyBindingStore = new RealmKeyBindingStore(realmFactory, keyCombinationProvider); + KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); dependencies.Cache(globalBindings); @@ -405,7 +405,7 @@ namespace osu.Game Scheduler.Add(() => { - realmBlocker = realmFactory.BlockAllOperations(); + realmBlocker = realm.BlockAllOperations(); readyToRun.Set(); }, false); @@ -483,7 +483,7 @@ namespace osu.Game BeatmapManager?.Dispose(); LocalConfig?.Dispose(); - realmFactory?.Dispose(); + realm?.Dispose(); } } } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 8df7ed9736..8450446473 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -69,7 +69,7 @@ namespace osu.Game.Overlays public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000)); [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } [BackgroundDependencyLoader] private void load() @@ -81,9 +81,9 @@ namespace osu.Game.Overlays } private IQueryable queryRealmBeatmapSets() => - realmFactory.Context - .All() - .Where(s => !s.DeletePending); + realm.Realm + .All() + .Where(s => !s.DeletePending); protected override void LoadComplete() { @@ -94,7 +94,7 @@ namespace osu.Game.Overlays foreach (var s in queryRealmBeatmapSets()) beatmapSets.Add(s.Detach()); - beatmapSubscription = realmFactory.RegisterForNotifications(realm => queryRealmBeatmapSets(), beatmapsChanged); + beatmapSubscription = realm.RegisterForNotifications(realm => queryRealmBeatmapSets(), beatmapsChanged); } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index c5854981e6..3b94cae171 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings protected override LocalisableString Header => DebugSettingsStrings.MemoryHeader; [BackgroundDependencyLoader] - private void load(GameHost host, RealmContextFactory realmFactory) + private void load(GameHost host, RealmAccess realm) { SettingsButton blockAction; SettingsButton unblockAction; @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings Action = () => { // Blocking operations implicitly causes a Compact(). - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) { } } @@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { try { - var token = realmFactory.BlockAllOperations(); + var token = realm.BlockAllOperations(); blockAction.Enabled.Value = false; diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 60aff91301..91883e4f41 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -386,7 +386,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void updateStoreFromButton(KeyButton button) { - realmFactory.Run(realm => + realm.Run(realm => { 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 5b8a52240e..922d371261 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -30,13 +30,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input } [BackgroundDependencyLoader] - private void load(RealmContextFactory realmFactory) + private void load(RealmAccess realm) { string rulesetName = Ruleset?.ShortName; - var bindings = realmFactory.Run(realm => realm.All() - .Where(b => b.RulesetName == rulesetName && b.Variant == variant) - .Detach()); + var bindings = realm.Run(r => r.All() + .Where(b => b.RulesetName == rulesetName && b.Variant == variant) + .Detach()); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 6e5cd0da2d..af3fd5c9bf 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -47,15 +47,15 @@ namespace osu.Game.Overlays.Settings.Sections private SkinManager skins { get; set; } [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } private IDisposable realmSubscription; private IQueryable queryRealmSkins() => - realmFactory.Context.All() - .Where(s => !s.DeletePending) - .OrderByDescending(s => s.Protected) // protected skins should be at the top. - .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase); + realm.Realm.All() + .Where(s => !s.DeletePending) + .OrderByDescending(s => s.Protected) // protected skins should be at the top. + .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase); [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor) @@ -83,7 +83,7 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Current = dropdownBindable; - realmSubscription = realmFactory.RegisterForNotifications(realm => queryRealmSkins(), (sender, changes, error) => + realmSubscription = realm.RegisterForNotifications(realm => queryRealmSkins(), (sender, changes, error) => { // The first fire of this is a bit redundant due to the call below, // but this is safest in case the subscription is restored after a context recycle. @@ -130,7 +130,7 @@ namespace osu.Game.Overlays.Settings.Sections { int protectedCount = queryRealmSkins().Count(s => s.Protected); - skinItems = queryRealmSkins().ToLive(realmFactory); + skinItems = queryRealmSkins().ToLive(realm); skinItems.Insert(protectedCount, random_skin_info); diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 75bebfa763..c855b76680 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Toolbar protected FillFlowContainer Flow; [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } protected ToolbarButton() : base(HoverSampleSet.Toolbar) @@ -207,7 +207,7 @@ namespace osu.Game.Overlays.Toolbar { if (Hotkey == null) return; - var realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value); + var realmKeyBinding = realm.Realm.All().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value); if (realmKeyBinding != null) { diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index 60a6b70221..cfa20e0b87 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Configuration public abstract class RulesetConfigManager : ConfigManager, IRulesetConfigManager where TLookup : struct, Enum { - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realm; private readonly int variant; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Configuration protected RulesetConfigManager(SettingsStore store, RulesetInfo ruleset, int? variant = null) { - realmFactory = store?.Realm; + realm = store?.Realm; rulesetName = ruleset.ShortName; @@ -37,10 +37,10 @@ namespace osu.Game.Rulesets.Configuration protected override void PerformLoad() { - if (realmFactory != null) + if (realm != null) { // As long as RulesetConfigCache exists, there is no need to subscribe to realm events. - databasedSettings = realmFactory.Context.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).ToList(); + databasedSettings = realm.Realm.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).ToList(); } } @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Configuration pendingWrites.Clear(); } - realmFactory?.Write(realm => + realm?.Write(realm => { foreach (var c in changed) { @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Configuration Variant = variant, }; - realmFactory?.Context.Write(() => realmFactory.Context.Add(setting)); + realm?.Realm.Write(() => realm.Realm.Add(setting)); databasedSettings.Add(setting); } diff --git a/osu.Game/Rulesets/RulesetConfigCache.cs b/osu.Game/Rulesets/RulesetConfigCache.cs index dee13e74a5..c4f1933cd8 100644 --- a/osu.Game/Rulesets/RulesetConfigCache.cs +++ b/osu.Game/Rulesets/RulesetConfigCache.cs @@ -13,14 +13,14 @@ namespace osu.Game.Rulesets { public class RulesetConfigCache : Component, IRulesetConfigCache { - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realm; private readonly RulesetStore rulesets; private readonly Dictionary configCache = new Dictionary(); - public RulesetConfigCache(RealmContextFactory realmFactory, RulesetStore rulesets) + public RulesetConfigCache(RealmAccess realm, RulesetStore rulesets) { - this.realmFactory = realmFactory; + this.realm = realm; this.rulesets = rulesets; } @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets { base.LoadComplete(); - var settingsStore = new SettingsStore(realmFactory); + var settingsStore = new SettingsStore(realm); // let's keep things simple for now and just retrieve all the required configs at startup.. foreach (var ruleset in rulesets.AvailableRulesets) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index a9e5ff797c..606bc65599 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets { public class RulesetStore : IDisposable, IRulesetStore { - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realm; private const string ruleset_library_prefix = @"osu.Game.Rulesets"; @@ -31,9 +31,9 @@ namespace osu.Game.Rulesets private readonly List availableRulesets = new List(); - public RulesetStore(RealmContextFactory realmFactory, Storage? storage = null) + public RulesetStore(RealmAccess realm, Storage? storage = null) { - this.realmFactory = realmFactory; + this.realm = realm; // On android in release configuration assemblies are loaded from the apk directly into memory. // We cannot read assemblies from cwd, so should check loaded assemblies instead. @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets private void addMissingRulesets() { - realmFactory.Write(realm => + realm.Write(realm => { var rulesets = realm.All(); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index f895134f97..e712d170cd 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -25,21 +25,21 @@ namespace osu.Game.Scoring { public class ScoreManager : IModelManager, IModelImporter { - private readonly RealmContextFactory contextFactory; + private readonly RealmAccess realm; private readonly Scheduler scheduler; private readonly Func difficulties; private readonly OsuConfigManager configManager; private readonly ScoreModelManager scoreModelManager; - public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmContextFactory contextFactory, Scheduler scheduler, + public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler, IIpcHost importHost = null, Func difficulties = null, OsuConfigManager configManager = null) { - this.contextFactory = contextFactory; + this.realm = realm; this.scheduler = scheduler; this.difficulties = difficulties; this.configManager = configManager; - scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, contextFactory); + scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, realm); } public Score GetScore(ScoreInfo score) => scoreModelManager.GetScore(score); @@ -51,7 +51,7 @@ namespace osu.Game.Scoring /// The first result for the provided query, or null if no results were found. public ScoreInfo Query(Expression> query) { - return contextFactory.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); + return realm.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); } /// @@ -254,7 +254,7 @@ namespace osu.Game.Scoring public void Delete([CanBeNull] Expression> filter = null, bool silent = false) { - contextFactory.Run(realm => + realm.Run(realm => { var items = realm.All() .Where(s => !s.DeletePending); diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 5e560effa1..2147ff1ba1 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -29,8 +29,8 @@ namespace osu.Game.Scoring private readonly RulesetStore rulesets; private readonly Func beatmaps; - public ScoreModelManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmContextFactory contextFactory) - : base(storage, contextFactory) + public ScoreModelManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm) + : base(storage, realm) { this.rulesets = rulesets; this.beatmaps = beatmaps; @@ -74,7 +74,7 @@ namespace osu.Game.Scoring public override bool IsAvailableLocally(ScoreInfo model) { - return ContextFactory.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); + return Access.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); } } } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index e66ecc74e1..fceb083916 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Menu private BeatmapManager beatmaps { get; set; } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, Framework.Game game, RealmContextFactory realmContextFactory) + private void load(OsuConfigManager config, Framework.Game game, RealmAccess realm) { // prevent user from changing beatmap while the intro is still running. beatmap = Beatmap.BeginLease(false); @@ -97,9 +97,9 @@ namespace osu.Game.Screens.Menu // if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection. if (!MenuMusic.Value) { - realmContextFactory.Run(realm => + realm.Run(r => { - var usableBeatmapSets = realm.All().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection(); + var usableBeatmapSets = r.All().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection(); int setCount = usableBeatmapSets.Count; diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5a6295fe74..8e0fdea0f8 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -179,24 +179,24 @@ namespace osu.Game.Screens.Select if (!loadedTestBeatmaps) { - realmFactory.Run(realm => loadBeatmapSets(getBeatmapSets(realm))); + realm.Run(realm => loadBeatmapSets(getBeatmapSets(realm))); } } [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } protected override void LoadComplete() { base.LoadComplete(); - subscriptionSets = realmFactory.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); - subscriptionBeatmaps = realmFactory.RegisterForNotifications(realm => realm.All().Where(b => !b.Hidden), beatmapsChanged); + subscriptionSets = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); + subscriptionBeatmaps = realm.RegisterForNotifications(realm => realm.All().Where(b => !b.Hidden), beatmapsChanged); // Can't use main subscriptions because we can't lookup deleted indices. // https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595. - subscriptionDeletedSets = realmFactory.RegisterForNotifications(realm => realm.All().Where(s => s.DeletePending && !s.Protected), deletedBeatmapSetsChanged); - subscriptionHiddenBeatmaps = realmFactory.RegisterForNotifications(realm => realm.All().Where(b => b.Hidden), beatmapsChanged); + subscriptionDeletedSets = realm.RegisterForNotifications(realm => realm.All().Where(s => s.DeletePending && !s.Protected), deletedBeatmapSetsChanged); + subscriptionHiddenBeatmaps = realm.RegisterForNotifications(realm => realm.All().Where(b => b.Hidden), beatmapsChanged); } private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) @@ -231,7 +231,7 @@ namespace osu.Game.Screens.Select foreach (var id in realmSets) { if (!root.BeatmapSetsByID.ContainsKey(id)) - UpdateBeatmapSet(realmFactory.Context.Find(id).Detach()); + UpdateBeatmapSet(realm.Realm.Find(id).Detach()); } foreach (var id in root.BeatmapSetsByID.Keys) diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index c03d464ef6..021dfd06f7 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Select.Carousel private IBindable ruleset { get; set; } [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } [Resolved] private IAPIProvider api { get; set; } @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Select.Carousel ruleset.BindValueChanged(_ => { scoreSubscription?.Dispose(); - scoreSubscription = realmFactory.RegisterForNotifications(realm => + scoreSubscription = realm.RegisterForNotifications(realm => realm.All() .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 5f288b972b..3d262f8b97 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select.Leaderboards private RulesetStore rulesets { get; set; } [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } private BeatmapInfo beatmapInfo; @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (beatmapInfo == null) return; - scoreSubscription = realmFactory.RegisterForNotifications(realm => + scoreSubscription = realm.RegisterForNotifications(realm => realm.All() .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID), (_, changes, ___) => @@ -150,7 +150,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { - realmFactory.Run(realm => + realm.Run(realm => { var scores = realm.All() .AsEnumerable() diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 4abde56ea6..3cf9f79611 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Spectate } [Resolved] - private RealmContextFactory realmContextFactory { get; set; } + private RealmAccess realm { get; set; } private IDisposable realmSubscription; @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Spectate playingUserStates.BindTo(spectatorClient.PlayingUserStates); playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); - realmSubscription = realmContextFactory.RegisterForNotifications( + realmSubscription = realm.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); foreach ((int id, var _) in userMap) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index d606d94b97..3685a26e26 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -43,8 +43,8 @@ namespace osu.Game.Skinning protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) { - SkinInfo = resources?.RealmContextFactory != null - ? skin.ToLive(resources.RealmContextFactory) + SkinInfo = resources?.RealmAccess != null + ? skin.ToLive(resources.RealmAccess) // This path should only be used in some tests. : skin.ToLiveUnmanaged(); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 82bcd3b292..66956325da 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -54,7 +54,7 @@ namespace osu.Game.Skinning }; private readonly SkinModelManager skinModelManager; - private readonly RealmContextFactory contextFactory; + private readonly RealmAccess realm; private readonly IResourceStore userFiles; @@ -68,9 +68,9 @@ namespace osu.Game.Skinning /// public Skin DefaultLegacySkin { get; } - public SkinManager(Storage storage, RealmContextFactory contextFactory, GameHost host, IResourceStore resources, AudioManager audio, Scheduler scheduler) + public SkinManager(Storage storage, RealmAccess realm, GameHost host, IResourceStore resources, AudioManager audio, Scheduler scheduler) { - this.contextFactory = contextFactory; + this.realm = realm; this.audio = audio; this.scheduler = scheduler; this.host = host; @@ -78,7 +78,7 @@ namespace osu.Game.Skinning userFiles = new StorageBackedResourceStore(storage.GetStorageForDirectory("files")); - skinModelManager = new SkinModelManager(storage, contextFactory, host, this); + skinModelManager = new SkinModelManager(storage, realm, host, this); var defaultSkins = new[] { @@ -87,7 +87,7 @@ namespace osu.Game.Skinning }; // Ensure the default entries are present. - contextFactory.Write(realm => + realm.Write(realm => { foreach (var skin in defaultSkins) { @@ -110,10 +110,10 @@ namespace osu.Game.Skinning public void SelectRandomSkin() { - contextFactory.Run(realm => + realm.Run(r => { // choose from only user skins, removing the current selection to ensure a new one is chosen. - var randomChoices = realm.All().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); + var randomChoices = r.All().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); if (randomChoices.Length == 0) { @@ -123,7 +123,7 @@ namespace osu.Game.Skinning var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); - CurrentSkinInfo.Value = chosen.ToLive(contextFactory); + CurrentSkinInfo.Value = chosen.ToLive(realm); }); } @@ -179,7 +179,7 @@ namespace osu.Game.Skinning /// The first result for the provided query, or null if no results were found. public ILive Query(Expression> query) { - return contextFactory.Run(realm => realm.All().FirstOrDefault(query)?.ToLive(contextFactory)); + return realm.Run(r => r.All().FirstOrDefault(query)?.ToLive(realm)); } public event Action SourceChanged; @@ -234,7 +234,7 @@ namespace osu.Game.Skinning AudioManager IStorageResourceProvider.AudioManager => audio; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.Files => userFiles; - RealmContextFactory IStorageResourceProvider.RealmContextFactory => contextFactory; + RealmAccess IStorageResourceProvider.RealmAccess => realm; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); #endregion @@ -289,7 +289,7 @@ namespace osu.Game.Skinning public void Delete([CanBeNull] Expression> filter = null, bool silent = false) { - contextFactory.Run(realm => + realm.Run(realm => { var items = realm.All() .Where(s => !s.Protected && !s.DeletePending); diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index a1926913a9..c93cdb17dd 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -27,8 +27,8 @@ namespace osu.Game.Skinning private readonly IStorageResourceProvider skinResources; - public SkinModelManager(Storage storage, RealmContextFactory contextFactory, GameHost host, IStorageResourceProvider skinResources) - : base(storage, contextFactory) + public SkinModelManager(Storage storage, RealmAccess realm, GameHost host, IStorageResourceProvider skinResources) + : base(storage, realm) { this.skinResources = skinResources; @@ -205,7 +205,7 @@ namespace osu.Game.Skinning private void populateMissingHashes() { - ContextFactory.Run(realm => + Access.Run(realm => { var skinsWithoutHashes = realm.All().Where(i => !i.Protected && string.IsNullOrEmpty(i.Hash)).ToArray(); diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 61178014ef..3d241e795c 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -44,8 +44,8 @@ namespace osu.Game.Stores private readonly BeatmapOnlineLookupQueue? onlineLookupQueue; - protected BeatmapImporter(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) - : base(storage, contextFactory) + protected BeatmapImporter(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) + : base(storage, realm) { this.onlineLookupQueue = onlineLookupQueue; } @@ -165,7 +165,7 @@ namespace osu.Game.Stores public override bool IsAvailableLocally(BeatmapSetInfo model) { - return ContextFactory.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); + return Access.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); } public override string HumanisedModelName => "beatmap"; diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 3d8e9f2703..23a860791e 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -59,7 +59,7 @@ namespace osu.Game.Stores protected readonly RealmFileStore Files; - protected readonly RealmContextFactory ContextFactory; + protected readonly RealmAccess Access; /// /// Fired when the user requests to view the resulting import. @@ -71,11 +71,11 @@ namespace osu.Game.Stores /// public Action? PostNotification { protected get; set; } - protected RealmArchiveModelImporter(Storage storage, RealmContextFactory contextFactory) + protected RealmArchiveModelImporter(Storage storage, RealmAccess realm) { - ContextFactory = contextFactory; + Access = realm; - Files = new RealmFileStore(contextFactory, storage); + Files = new RealmFileStore(realm, storage); } /// @@ -320,7 +320,7 @@ namespace osu.Game.Stores /// An optional cancellation token. public virtual Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { - return ContextFactory.Run(realm => + return Access.Run(realm => { cancellationToken.ThrowIfCancellationRequested(); @@ -352,7 +352,7 @@ namespace osu.Game.Stores transaction.Commit(); } - return Task.FromResult((ILive?)existing.ToLive(ContextFactory)); + return Task.FromResult((ILive?)existing.ToLive(Access)); } LogForModel(item, @"Found existing (optimised) but failed pre-check."); @@ -387,7 +387,7 @@ namespace osu.Game.Stores existing.DeletePending = false; transaction.Commit(); - return Task.FromResult((ILive?)existing.ToLive(ContextFactory)); + return Task.FromResult((ILive?)existing.ToLive(Access)); } LogForModel(item, @"Found existing but failed re-use check."); @@ -413,7 +413,7 @@ namespace osu.Game.Stores throw; } - return Task.FromResult((ILive?)item.ToLive(ContextFactory)); + return Task.FromResult((ILive?)item.ToLive(Access)); }); } diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 115fbf721d..01eb90d6e8 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -24,10 +24,10 @@ namespace osu.Game.Stores { private readonly RealmFileStore realmFileStore; - protected RealmArchiveModelManager(Storage storage, RealmContextFactory contextFactory) - : base(storage, contextFactory) + protected RealmArchiveModelManager(Storage storage, RealmAccess realm) + : base(storage, realm) { - realmFileStore = new RealmFileStore(contextFactory, storage); + realmFileStore = new RealmFileStore(realm, storage); } public void DeleteFile(TModel item, RealmNamedFileUsage file) => @@ -45,7 +45,7 @@ namespace osu.Game.Stores // This method should be removed as soon as all the surrounding pieces support non-detached operations. if (!item.IsManaged) { - var managed = ContextFactory.Context.Find(item.ID); + var managed = Access.Context.Find(item.ID); managed.Realm.Write(() => operation(managed)); item.Files.Clear(); @@ -165,7 +165,7 @@ namespace osu.Game.Stores public bool Delete(TModel item) { - return ContextFactory.Run(realm => + return Access.Run(realm => { if (!item.IsManaged) item = realm.Find(item.ID); @@ -180,7 +180,7 @@ namespace osu.Game.Stores public void Undelete(TModel item) { - ContextFactory.Run(realm => + Access.Run(realm => { if (!item.IsManaged) item = realm.Find(item.ID); diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs index ca371e29be..5edc1be954 100644 --- a/osu.Game/Stores/RealmFileStore.cs +++ b/osu.Game/Stores/RealmFileStore.cs @@ -24,15 +24,15 @@ namespace osu.Game.Stores [ExcludeFromDynamicCompile] public class RealmFileStore { - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realm; public readonly IResourceStore Store; public readonly Storage Storage; - public RealmFileStore(RealmContextFactory realmFactory, Storage storage) + public RealmFileStore(RealmAccess realm, Storage storage) { - this.realmFactory = realmFactory; + this.realm = realm; Storage = storage.GetStorageForDirectory(@"files"); Store = new StorageBackedResourceStore(Storage); @@ -92,7 +92,7 @@ namespace osu.Game.Stores int removedFiles = 0; // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. - realmFactory.Write(realm => + realm.Write(realm => { // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707) var files = realm.All().ToList(); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 3d6240bc98..e6528a83bd 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -77,12 +77,12 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmContextFactory realmContextFactory) + private void load(GameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmAccess realm) { if (clock != null) Clock = clock; - dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(new RealmFileStore(realmContextFactory, host.Storage).Store), false, scaleAdjust: 1)); + dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(new RealmFileStore(realm, host.Storage).Store), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) { diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 10cb210f4d..f7e154b5e7 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Beatmaps public IResourceStore Files => userSkinResourceStore; public new IResourceStore Resources => base.Resources; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; - RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; + RealmAccess IStorageResourceProvider.RealmAccess => null; #endregion diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 6cc009514d..a6cac0ec86 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual working = CreateWorkingBeatmap(Ruleset.Value); if (IsolateSavingFromDatabase) - Dependencies.CacheAs(testBeatmapManager = new TestBeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.CacheAs(testBeatmapManager = new TestBeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); } protected override void LoadComplete() @@ -126,14 +126,14 @@ namespace osu.Game.Tests.Visual { public WorkingBeatmap TestBeatmap; - public TestBeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host, WorkingBeatmap defaultBeatmap) - : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) + public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host, WorkingBeatmap defaultBeatmap) + : base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap) { } - protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) { - return new TestBeatmapModelManager(storage, contextFactory, rulesets, onlineLookupQueue); + return new TestBeatmapModelManager(storage, realm, rulesets, onlineLookupQueue); } protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) @@ -157,8 +157,8 @@ namespace osu.Game.Tests.Visual internal class TestBeatmapModelManager : BeatmapModelManager { - public TestBeatmapModelManager(Storage storage, RealmContextFactory databaseContextFactory, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) - : base(databaseContextFactory, storage, beatmapOnlineLookupQueue) + public TestBeatmapModelManager(Storage storage, RealmAccess databaseAccess, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) + : base(databaseAccess, storage, beatmapOnlineLookupQueue) { } diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index da8af49158..33dd1d45b8 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -75,9 +75,9 @@ namespace osu.Game.Tests.Visual /// /// In interactive runs (ie. VisualTests) this will use the user's database if is not set to true. /// - protected RealmContextFactory ContextFactory => contextFactory.Value; + protected RealmAccess Access => contextFactory.Value; - private Lazy contextFactory; + private Lazy contextFactory; /// /// Whether a fresh storage should be initialised per test (method) run. @@ -119,7 +119,7 @@ namespace osu.Game.Tests.Visual Resources = parent.Get().Resources; - contextFactory = new Lazy(() => new RealmContextFactory(LocalStorage, "client")); + contextFactory = new Lazy(() => new RealmAccess(LocalStorage, "client")); RecycleLocalStorage(false); diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index a080f47d66..cd675e467b 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual public IResourceStore Files => null; public new IResourceStore Resources => base.Resources; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); - RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; + RealmAccess IStorageResourceProvider.RealmAccess => null; #endregion From f30894840c709505464cc2b903f4a8870feb7fd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 20:11:36 +0900 Subject: [PATCH 101/587] Update terminology to realm "instance" rather than "context" This matches the terminology used by realm themselves, which feels better. --- osu.Game.Tests/Database/FileStoreTests.cs | 8 +- .../TestSceneChangeAndUseGameplayBindings.cs | 2 +- osu.Game/Database/RealmAccess.cs | 131 ++++++++---------- osu.Game/Stores/RealmArchiveModelManager.cs | 2 +- 4 files changed, 67 insertions(+), 76 deletions(-) diff --git a/osu.Game.Tests/Database/FileStoreTests.cs b/osu.Game.Tests/Database/FileStoreTests.cs index 008aa3d833..98b0ed99b5 100644 --- a/osu.Game.Tests/Database/FileStoreTests.cs +++ b/osu.Game.Tests/Database/FileStoreTests.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmAccess, storage) => { - var realm = realmAccess.Context; + var realm = realmAccess.Realm; var files = new RealmFileStore(realmAccess, storage); var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmAccess, storage) => { - var realm = realmAccess.Context; + var realm = realmAccess.Realm; var files = new RealmFileStore(realmAccess, storage); var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmAccess, storage) => { - var realm = realmAccess.Context; + var realm = realmAccess.Realm; var files = new RealmFileStore(realmAccess, storage); var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmAccess, storage) => { - var realm = realmAccess.Context; + var realm = realmAccess.Realm; var files = new RealmFileStore(realmAccess, storage); var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index 7cb29895eb..bfcefdbbfe 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Navigation .ChildrenOfType().SingleOrDefault(); private RealmKeyBinding firstOsuRulesetKeyBindings => Game.Dependencies - .Get().Context + .Get().Realm .All() .AsEnumerable() .First(k => k.RulesetName == "osu" && k.ActionInt == 0); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 2f397639c9..66b4edbe84 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -59,9 +59,9 @@ namespace osu.Game.Database /// /// Lock object which is held during sections, blocking context creation during blocking periods. /// - private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1); + private readonly SemaphoreSlim realmCreationLock = new SemaphoreSlim(1); - private readonly ThreadLocal currentThreadCanCreateContexts = new ThreadLocal(); + private readonly ThreadLocal currentThreadCanCreateRealmInstances = new ThreadLocal(); /// /// Holds a map of functions registered via and and a coinciding action which when triggered, @@ -81,35 +81,35 @@ namespace osu.Game.Database /// private readonly Dictionary, Action> notificationsResetMap = new Dictionary, Action>(); - private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)"); + private static readonly GlobalStatistic realm_instances_created = GlobalStatistics.Get(@"Realm", @"Instances (Created)"); - private readonly object contextLock = new object(); + private readonly object realmLock = new object(); - private Realm? context; + private Realm? updateRealm; - public Realm Context => ensureUpdateContext(); + public Realm Realm => ensureUpdateRealm(); - private Realm ensureUpdateContext() + private Realm ensureUpdateRealm() { if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException(@$"Use {nameof(createContext)} when performing realm operations from a non-update thread"); + throw new InvalidOperationException(@$"Use {nameof(getRealmInstance)} when performing realm operations from a non-update thread"); - lock (contextLock) + lock (realmLock) { - if (context == null) + if (updateRealm == null) { - context = createContext(); - Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}"); + updateRealm = getRealmInstance(); + + Logger.Log(@$"Opened realm ""{updateRealm.Config.DatabasePath}"" at version {updateRealm.Config.SchemaVersion}"); // Resubscribe any subscriptions foreach (var action in customSubscriptionsResetMap.Keys) registerSubscription(action); } - Debug.Assert(context != null); + Debug.Assert(updateRealm != null); - // creating a context will ensure our schema is up-to-date and migrated. - return context; + return updateRealm; } } @@ -118,7 +118,7 @@ namespace osu.Game.Database private static readonly ThreadLocal current_thread_subscriptions_allowed = new ThreadLocal(); /// - /// Construct a new instance of a realm context factory. + /// Construct a new instance. /// /// The game storage which will be used to create the realm backing file. /// The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified. @@ -137,7 +137,7 @@ namespace osu.Game.Database try { - // This method triggers the first `CreateContext` call, which will implicitly run realm migrations and bring the schema up-to-date. + // This method triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date. cleanupPendingDeletions(); } catch (Exception e) @@ -153,7 +153,7 @@ namespace osu.Game.Database private void cleanupPendingDeletions() { - using (var realm = createContext()) + using (var realm = getRealmInstance()) using (var transaction = realm.BeginWrite()) { var pendingDeleteScores = realm.All().Where(s => s.DeletePending); @@ -201,34 +201,28 @@ namespace osu.Game.Database /// /// Run work on realm with a return value. /// - /// - /// Handles correct context management automatically. - /// /// The work to run. /// The return type. public T Run(Func action) { if (ThreadSafety.IsUpdateThread) - return action(Context); + return action(Realm); - using (var realm = createContext()) + using (var realm = getRealmInstance()) return action(realm); } /// /// Run work on realm. /// - /// - /// Handles correct context management automatically. - /// /// The work to run. public void Run(Action action) { if (ThreadSafety.IsUpdateThread) - action(Context); + action(Realm); else { - using (var realm = createContext()) + using (var realm = getRealmInstance()) action(realm); } } @@ -236,17 +230,14 @@ namespace osu.Game.Database /// /// Write changes to realm. /// - /// - /// Handles correct context management and transaction committing automatically. - /// /// The work to run. public void Write(Action action) { if (ThreadSafety.IsUpdateThread) - Context.Write(action); + Realm.Write(action); else { - using (var realm = createContext()) + using (var realm = getRealmInstance()) realm.Write(action); } } @@ -257,10 +248,10 @@ namespace osu.Game.Database /// /// This adds osu! specific thread and managed state safety checks on top of . /// - /// In addition to the documented realm behaviour, we have the additional requirement of handling subscriptions over potential context loss. + /// In addition to the documented realm behaviour, we have the additional requirement of handling subscriptions over potential realm instance recycle. /// When this happens, callback events will be automatically fired: - /// - On context loss, a callback with an empty collection and null will be invoked. - /// - On context revival, a standard initial realm callback will arrive, with null and an up-to-date collection. + /// - On recycle start, a callback with an empty collection and null will be invoked. + /// - On recycle end, a standard initial realm callback will arrive, with null and an up-to-date collection. /// /// The to observe for changes. /// Type of the elements in the list. @@ -276,7 +267,7 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); - lock (contextLock) + lock (realmLock) { Func action = realm => query(realm).QueryAsyncWithNotifications(callback); @@ -287,7 +278,7 @@ namespace osu.Game.Database } /// - /// Run work on realm that will be run every time the update thread realm context gets recycled. + /// Run work on realm that will be run every time the update thread realm instance gets recycled. /// /// The work to run. Return value should be an from QueryAsyncWithNotifications, or an to clean up any bindings. /// An which should be disposed to unsubscribe any inner subscription. @@ -311,7 +302,7 @@ namespace osu.Game.Database void unsubscribe() { - lock (contextLock) + lock (realmLock) { if (customSubscriptionsResetMap.TryGetValue(action, out var unsubscriptionAction)) { @@ -328,12 +319,12 @@ namespace osu.Game.Database { Debug.Assert(ThreadSafety.IsUpdateThread); - lock (contextLock) + lock (realmLock) { - // Retrieve context outside of flag update to ensure that the context is constructed, + // Retrieve realm instance outside of flag update to ensure that the instance is retrieved, // as attempting to access it inside the subscription if it's not constructed would lead to // cyclic invocations of the subscription callback. - var realm = Context; + var realm = Realm; Debug.Assert(!customSubscriptionsResetMap.TryGetValue(action, out var found) || found == null); @@ -344,12 +335,12 @@ namespace osu.Game.Database } /// - /// Unregister all subscriptions when the realm context is to be recycled. - /// Subscriptions will still remain and will be re-subscribed when the realm context returns. + /// Unregister all subscriptions when the realm instance is to be recycled. + /// Subscriptions will still remain and will be re-subscribed when the realm instance returns. /// private void unregisterAllSubscriptions() { - lock (contextLock) + lock (realmLock) { foreach (var action in notificationsResetMap.Values) action(); @@ -362,7 +353,7 @@ namespace osu.Game.Database } } - private Realm createContext() + private Realm getRealmInstance() { if (isDisposed) throw new ObjectDisposedException(nameof(RealmAccess)); @@ -371,20 +362,20 @@ namespace osu.Game.Database try { - if (!currentThreadCanCreateContexts.Value) + if (!currentThreadCanCreateRealmInstances.Value) { - contextCreationLock.Wait(); - currentThreadCanCreateContexts.Value = true; + realmCreationLock.Wait(); + currentThreadCanCreateRealmInstances.Value = true; tookSemaphoreLock = true; } else { - // the semaphore is used to handle blocking of all context creation during certain periods. - // once the semaphore has been taken by this code section, it is safe to create further contexts on the same thread. - // this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`. + // the semaphore is used to handle blocking of all realm retrieval during certain periods. + // once the semaphore has been taken by this code section, it is safe to retrieve further realm instances on the same thread. + // this can happen if a realm subscription is active and triggers a callback which has user code that calls `Run`. } - contexts_created.Value++; + realm_instances_created.Value++; return Realm.GetInstance(getConfiguration()); } @@ -392,8 +383,8 @@ namespace osu.Game.Database { if (tookSemaphoreLock) { - contextCreationLock.Release(); - currentThreadCanCreateContexts.Value = false; + realmCreationLock.Release(); + currentThreadCanCreateRealmInstances.Value = false; } } } @@ -582,7 +573,7 @@ namespace osu.Game.Database } /// - /// Flush any active contexts and block any further writes. + /// Flush any active realm instances and block any further writes. /// /// /// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm. @@ -598,14 +589,14 @@ namespace osu.Game.Database try { - contextCreationLock.Wait(); + realmCreationLock.Wait(); - lock (contextLock) + lock (realmLock) { - if (context == null) + if (updateRealm == null) { - // null context means the update thread has not yet retrieved its context. - // we don't need to worry about reviving the update context in this case, so don't bother with the SynchronizationContext. + // null realm means the update thread has not yet retrieved its instance. + // we don't need to worry about reviving the update instance in this case, so don't bother with the SynchronizationContext. Debug.Assert(!ThreadSafety.IsUpdateThread); } else @@ -620,8 +611,8 @@ namespace osu.Game.Database Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); - context?.Dispose(); - context = null; + updateRealm?.Dispose(); + updateRealm = null; } const int sleep_length = 200; @@ -648,17 +639,17 @@ namespace osu.Game.Database } catch { - contextCreationLock.Release(); + realmCreationLock.Release(); throw; } return new InvokeOnDisposal(this, factory => { - factory.contextCreationLock.Release(); + factory.realmCreationLock.Release(); Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); // Post back to the update thread to revive any subscriptions. - syncContext?.Post(_ => ensureUpdateContext(), null); + syncContext?.Post(_ => ensureUpdateRealm(), null); }); } @@ -669,16 +660,16 @@ namespace osu.Game.Database public void Dispose() { - lock (contextLock) + lock (realmLock) { - context?.Dispose(); + updateRealm?.Dispose(); } if (!isDisposed) { // intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal. - contextCreationLock.Wait(); - contextCreationLock.Dispose(); + realmCreationLock.Wait(); + realmCreationLock.Dispose(); isDisposed = true; } diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 01eb90d6e8..00cd1c2958 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -45,7 +45,7 @@ namespace osu.Game.Stores // This method should be removed as soon as all the surrounding pieces support non-detached operations. if (!item.IsManaged) { - var managed = Access.Context.Find(item.ID); + var managed = Access.Realm.Find(item.ID); managed.Realm.Write(() => operation(managed)); item.Files.Clear(); From 6f4c337a5696ef2e51c10511ceeed873cd0834b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 20:52:27 +0900 Subject: [PATCH 102/587] Fix a failed `BlockAllOperations` leaving update realm in unretrieved state If the operation timed out on.. ```csharp throw new TimeoutException(@"Took too long to acquire lock"); ``` ..from an update thread, it would not restore the update context. The next call would then fail on the assert that ensures a non-null context in such cases. Can add test coverage if required. --- osu.Game/Database/RealmAccess.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 66b4edbe84..64063664eb 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -595,8 +595,8 @@ namespace osu.Game.Database { if (updateRealm == null) { - // null realm means the update thread has not yet retrieved its instance. - // we don't need to worry about reviving the update instance in this case, so don't bother with the SynchronizationContext. + // null context means the update thread has not yet retrieved its context. + // we don't need to worry about reviving the update context in this case, so don't bother with the SynchronizationContext. Debug.Assert(!ThreadSafety.IsUpdateThread); } else @@ -639,18 +639,19 @@ namespace osu.Game.Database } catch { - realmCreationLock.Release(); + restoreOperation(); throw; } - return new InvokeOnDisposal(this, factory => - { - factory.realmCreationLock.Release(); - Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); + return new InvokeOnDisposal(restoreOperation); + void restoreOperation() + { + Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); + realmCreationLock.Release(); // Post back to the update thread to revive any subscriptions. syncContext?.Post(_ => ensureUpdateRealm(), null); - }); + } } // https://github.com/realm/realm-dotnet/blob/32f4ebcc88b3e80a3b254412665340cd9f3bd6b5/Realm/Realm/Extensions/ReflectionExtensions.cs#L46 From a7c0d507cefa16343475f5aa73a23d666b6b7446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Jan 2022 20:44:25 +0100 Subject: [PATCH 103/587] Rename flashlight settings to be more accurate --- .../Mods/CatchModFlashlight.cs | 20 +++++++++---------- .../Mods/ManiaModFlashlight.cs | 20 +++++++++---------- .../Mods/OsuModFlashlight.cs | 20 +++++++++---------- .../Mods/TaikoModFlashlight.cs | 20 +++++++++---------- osu.Game/Rulesets/Mods/ModFlashlight.cs | 8 ++++---- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index d48382a9ee..e7335dcc34 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -16,15 +16,8 @@ namespace osu.Game.Rulesets.Catch.Mods { public override double ScoreMultiplier => 1.12; - [SettingSource("Change radius based on combo", "Decrease the flashlight radius as combo increases.")] - public override BindableBool ChangeRadius { get; } = new BindableBool - { - Default = true, - Value = true - }; - - [SettingSource("Initial radius", "Initial radius of the flashlight area.")] - public override BindableNumber InitialRadius { get; } = new BindableNumber + [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] + public override BindableNumber SizeMultiplier { get; } = new BindableNumber { MinValue = 0.4f, MaxValue = 1.7f, @@ -33,9 +26,16 @@ namespace osu.Game.Rulesets.Catch.Mods Precision = 0.1f }; + [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] + public override BindableBool ComboBasedSize { get; } = new BindableBool + { + Default = true, + Value = true + }; + protected virtual float DefaultFlashlightSize => 350; - public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, DefaultFlashlightSize); + public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ComboBasedSize.Value, SizeMultiplier.Value, DefaultFlashlightSize); private CatchPlayfield playfield; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index eb3f60edce..f89c131fea 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -17,15 +17,8 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModHidden) }; - [SettingSource("Change radius based on combo", "Decrease the flashlight radius as combo increases.")] - public override BindableBool ChangeRadius { get; } = new BindableBool - { - Default = false, - Value = false - }; - - [SettingSource("Initial radius", "Initial radius of the flashlight area.")] - public override BindableNumber InitialRadius { get; } = new BindableNumber + [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] + public override BindableNumber SizeMultiplier { get; } = new BindableNumber { MinValue = 0f, MaxValue = 4.5f, @@ -34,9 +27,16 @@ namespace osu.Game.Rulesets.Mania.Mods Precision = 0.1f }; + [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] + public override BindableBool ComboBasedSize { get; } = new BindableBool + { + Default = false, + Value = false + }; + protected virtual float DefaultFlashlightSize => 50; - public override Flashlight CreateFlashlight() => new ManiaFlashlight(ChangeRadius.Value, InitialRadius.Value, DefaultFlashlightSize); + public override Flashlight CreateFlashlight() => new ManiaFlashlight(ComboBasedSize.Value, SizeMultiplier.Value, DefaultFlashlightSize); private class ManiaFlashlight : Flashlight { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index 6a9d199c54..bc915591d0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -30,15 +30,8 @@ namespace osu.Game.Rulesets.Osu.Mods Precision = default_follow_delay, }; - [SettingSource("Change radius based on combo", "Decrease the flashlight radius as combo increases.")] - public override BindableBool ChangeRadius { get; } = new BindableBool - { - Default = true, - Value = true - }; - - [SettingSource("Initial radius", "Initial radius of the flashlight area.")] - public override BindableNumber InitialRadius { get; } = new BindableNumber + [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] + public override BindableNumber SizeMultiplier { get; } = new BindableNumber { MinValue = 0.5f, MaxValue = 2f, @@ -47,11 +40,18 @@ namespace osu.Game.Rulesets.Osu.Mods Precision = 0.1f }; + [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] + public override BindableBool ComboBasedSize { get; } = new BindableBool + { + Default = true, + Value = true + }; + protected virtual float DefaultFlashlightSize => 180; private OsuFlashlight flashlight; - public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(ChangeRadius.Value, InitialRadius.Value, FollowDelay.Value, DefaultFlashlightSize); + public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(ComboBasedSize.Value, SizeMultiplier.Value, FollowDelay.Value, DefaultFlashlightSize); public void ApplyToDrawableHitObject(DrawableHitObject drawable) { diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index 8de7c859c4..48e56c8784 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -17,15 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override double ScoreMultiplier => 1.12; - [SettingSource("Change radius based on combo", "Decrease the flashlight radius as combo increases.")] - public override BindableBool ChangeRadius { get; } = new BindableBool - { - Default = true, - Value = true - }; - - [SettingSource("Initial radius", "Initial radius of the flashlight area.")] - public override BindableNumber InitialRadius { get; } = new BindableNumber + [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] + public override BindableNumber SizeMultiplier { get; } = new BindableNumber { MinValue = 0, MaxValue = 1.66f, @@ -34,9 +27,16 @@ namespace osu.Game.Rulesets.Taiko.Mods Precision = 0.1f }; + [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] + public override BindableBool ComboBasedSize { get; } = new BindableBool + { + Default = true, + Value = true + }; + protected virtual float DefaultFlashlightSize => 250; - public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ChangeRadius.Value, InitialRadius.Value, DefaultFlashlightSize); + public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ComboBasedSize.Value, SizeMultiplier.Value, DefaultFlashlightSize); private TaikoPlayfield playfield; diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 531ee92b7a..d3998bcad4 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyIncrease; public override string Description => "Restricted view area."; - [SettingSource("Change radius based on combo", "Decrease the flashlight radius as combo increases.")] - public abstract BindableBool ChangeRadius { get; } + [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] + public abstract BindableNumber SizeMultiplier { get; } - [SettingSource("Initial radius", "Initial radius of the flashlight area.")] - public abstract BindableNumber InitialRadius { get; } + [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] + public abstract BindableBool ComboBasedSize { get; } } public abstract class ModFlashlight : ModFlashlight, IApplicableToDrawableRuleset, IApplicableToScoreProcessor From 5874475dffbc5ba6d1bf620d07e026360858a3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Jan 2022 20:46:54 +0100 Subject: [PATCH 104/587] Extract `DefaultFlashlightSize` to base flashlight class --- osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs | 2 +- osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs | 2 +- osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs | 2 +- osu.Game/Rulesets/Mods/ModFlashlight.cs | 6 ++++++ 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index e7335dcc34..4c5dcf84d2 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Mods Value = true }; - protected virtual float DefaultFlashlightSize => 350; + public override float DefaultFlashlightSize => 350; public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ComboBasedSize.Value, SizeMultiplier.Value, DefaultFlashlightSize); diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index f89c131fea..03be00c5db 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods Value = false }; - protected virtual float DefaultFlashlightSize => 50; + public override float DefaultFlashlightSize => 50; public override Flashlight CreateFlashlight() => new ManiaFlashlight(ComboBasedSize.Value, SizeMultiplier.Value, DefaultFlashlightSize); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index bc915591d0..f344bb017e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Mods Value = true }; - protected virtual float DefaultFlashlightSize => 180; + public override float DefaultFlashlightSize => 180; private OsuFlashlight flashlight; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index 48e56c8784..576fbb65a0 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Mods Value = true }; - protected virtual float DefaultFlashlightSize => 250; + public override float DefaultFlashlightSize => 250; public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ComboBasedSize.Value, SizeMultiplier.Value, DefaultFlashlightSize); diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index d3998bcad4..f2fd86d549 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -38,6 +38,12 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] public abstract BindableBool ComboBasedSize { get; } + + /// + /// The default size of the flashlight in ruleset-appropriate dimensions. + /// and will apply their adjustments on top of this size. + /// + public abstract float DefaultFlashlightSize { get; } } public abstract class ModFlashlight : ModFlashlight, IApplicableToDrawableRuleset, IApplicableToScoreProcessor From a227af75edb70a847aac468c47371bacc4d3d49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Jan 2022 20:55:24 +0100 Subject: [PATCH 105/587] Simplify flashlight parameter passing flow --- .../Mods/CatchModFlashlight.cs | 10 +++---- .../Mods/ManiaModFlashlight.cs | 10 +++---- .../Mods/OsuModFlashlight.cs | 19 +++++------- .../Mods/TaikoModFlashlight.cs | 8 ++--- osu.Game/Rulesets/Mods/ModFlashlight.cs | 30 +++++++++---------- 5 files changed, 37 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index 4c5dcf84d2..6bd914de33 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.Mods public override float DefaultFlashlightSize => 350; - public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield, ComboBasedSize.Value, SizeMultiplier.Value, DefaultFlashlightSize); + protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield); private CatchPlayfield playfield; @@ -49,11 +49,11 @@ namespace osu.Game.Rulesets.Catch.Mods { private readonly CatchPlayfield playfield; - public CatchFlashlight(CatchPlayfield playfield, bool isRadiusBasedOnCombo, float initialRadius, float defaultFlashlightSize) - : base(isRadiusBasedOnCombo, initialRadius, defaultFlashlightSize) + public CatchFlashlight(CatchModFlashlight modFlashlight, CatchPlayfield playfield) + : base(modFlashlight) { this.playfield = playfield; - FlashlightSize = new Vector2(0, GetRadiusFor(0)); + FlashlightSize = new Vector2(0, GetSizeFor(0)); } protected override void Update() @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Catch.Mods protected override void OnComboChange(ValueChangedEvent e) { - this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetRadiusFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); + this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); } protected override string FragmentShader => "CircularFlashlight"; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index 03be00c5db..def3f8b274 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -36,16 +36,16 @@ namespace osu.Game.Rulesets.Mania.Mods public override float DefaultFlashlightSize => 50; - public override Flashlight CreateFlashlight() => new ManiaFlashlight(ComboBasedSize.Value, SizeMultiplier.Value, DefaultFlashlightSize); + protected override Flashlight CreateFlashlight() => new ManiaFlashlight(this); private class ManiaFlashlight : Flashlight { private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); - public ManiaFlashlight(bool isRadiusBasedOnCombo, float initialRadius, float defaultFlashlightSize) - : base(isRadiusBasedOnCombo, initialRadius, defaultFlashlightSize) + public ManiaFlashlight(ManiaModFlashlight modFlashlight) + : base(modFlashlight) { - FlashlightSize = new Vector2(DrawWidth, GetRadiusFor(0)); + FlashlightSize = new Vector2(DrawWidth, GetSizeFor(0)); AddLayout(flashlightProperties); } @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.Mods protected override void OnComboChange(ValueChangedEvent e) { - this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, GetRadiusFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); + this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); } protected override string FragmentShader => "RectangularFlashlight"; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index f344bb017e..b4eff57c55 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Mods private OsuFlashlight flashlight; - public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(ComboBasedSize.Value, SizeMultiplier.Value, FollowDelay.Value, DefaultFlashlightSize); + protected override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(this); public void ApplyToDrawableHitObject(DrawableHitObject drawable) { @@ -61,17 +61,14 @@ namespace osu.Game.Rulesets.Osu.Mods private class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition { - public double FollowDelay { private get; set; } + private readonly double followDelay; - //public float InitialRadius { private get; set; } - public bool ChangeRadius { private get; set; } - - public OsuFlashlight(bool isRadiusBasedOnCombo, float initialRadius, double followDelay, float defaultFlashlightSize) - : base(isRadiusBasedOnCombo, initialRadius, defaultFlashlightSize) + public OsuFlashlight(OsuModFlashlight modFlashlight) + : base(modFlashlight) { - FollowDelay = followDelay; + followDelay = modFlashlight.FollowDelay.Value; - FlashlightSize = new Vector2(0, GetRadiusFor(0)); + FlashlightSize = new Vector2(0, GetSizeFor(0)); } public void OnSliderTrackingChange(ValueChangedEvent e) @@ -86,14 +83,14 @@ namespace osu.Game.Rulesets.Osu.Mods var destination = e.MousePosition; FlashlightPosition = Interpolation.ValueAt( - Math.Min(Math.Abs(Clock.ElapsedFrameTime), FollowDelay), position, destination, 0, FollowDelay, Easing.Out); + Math.Min(Math.Abs(Clock.ElapsedFrameTime), followDelay), position, destination, 0, followDelay, Easing.Out); return base.OnMouseMove(e); } protected override void OnComboChange(ValueChangedEvent e) { - this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetRadiusFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); + this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); } protected override string FragmentShader => "CircularFlashlight"; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index 576fbb65a0..84444dded9 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public override float DefaultFlashlightSize => 250; - public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield, ComboBasedSize.Value, SizeMultiplier.Value, DefaultFlashlightSize); + protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, playfield); private TaikoPlayfield playfield; @@ -51,8 +51,8 @@ namespace osu.Game.Rulesets.Taiko.Mods private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); private readonly TaikoPlayfield taikoPlayfield; - public TaikoFlashlight(TaikoPlayfield taikoPlayfield, bool isRadiusBasedOnCombo, float initialRadius, float defaultFlashlightSize) - : base(isRadiusBasedOnCombo, initialRadius, defaultFlashlightSize) + public TaikoFlashlight(TaikoModFlashlight modFlashlight, TaikoPlayfield taikoPlayfield) + : base(modFlashlight) { this.taikoPlayfield = taikoPlayfield; FlashlightSize = getSizeFor(0); @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Taiko.Mods private Vector2 getSizeFor(int combo) { // Preserve flashlight size through the playfield's aspect adjustment. - return new Vector2(0, GetRadiusFor(combo) * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); + return new Vector2(0, GetSizeFor(combo) * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); } protected override void OnComboChange(ValueChangedEvent e) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index f2fd86d549..e6487c6b29 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Mods flashlight.Breaks = drawableRuleset.Beatmap.Breaks; } - public abstract Flashlight CreateFlashlight(); + protected abstract Flashlight CreateFlashlight(); public abstract class Flashlight : Drawable { @@ -102,17 +102,15 @@ namespace osu.Game.Rulesets.Mods public List Breaks; - public readonly bool IsRadiusBasedOnCombo; + private readonly float defaultFlashlightSize; + private readonly float sizeMultiplier; + private readonly bool comboBasedSize; - public readonly float InitialRadius; - - public readonly float DefaultFlashlightSize; - - protected Flashlight(bool isRadiusBasedOnCombo, float initialRadius, float defaultFlashlightSize) + protected Flashlight(ModFlashlight modFlashlight) { - IsRadiusBasedOnCombo = isRadiusBasedOnCombo; - InitialRadius = initialRadius; - DefaultFlashlightSize = defaultFlashlightSize; + defaultFlashlightSize = modFlashlight.DefaultFlashlightSize; + sizeMultiplier = modFlashlight.SizeMultiplier.Value; + comboBasedSize = modFlashlight.ComboBasedSize.Value; } [BackgroundDependencyLoader] @@ -146,17 +144,19 @@ namespace osu.Game.Rulesets.Mods protected abstract string FragmentShader { get; } - protected float GetRadiusFor(int combo) + protected float GetSizeFor(int combo) { - if (IsRadiusBasedOnCombo) + float size = defaultFlashlightSize * sizeMultiplier; + + if (comboBasedSize) { if (combo > 200) - return InitialRadius * 0.8f * DefaultFlashlightSize; + size *= 0.8f; else if (combo > 100) - return InitialRadius * 0.9f * DefaultFlashlightSize; + size *= 0.9f; } - return InitialRadius * DefaultFlashlightSize; + return size; } private Vector2 flashlightPosition; From 4a13c93ca7e98c89ba5938c89c1e00a2856c08c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Jan 2022 20:56:59 +0100 Subject: [PATCH 106/587] Disallow zero size multiplier in flashlight implementations --- osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs | 2 +- osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index def3f8b274..60de063b24 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Mods [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] public override BindableNumber SizeMultiplier { get; } = new BindableNumber { - MinValue = 0f, + MinValue = 0.1f, MaxValue = 4.5f, Default = 1f, Value = 1f, diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index 84444dded9..bdb30e9ded 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Mods [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] public override BindableNumber SizeMultiplier { get; } = new BindableNumber { - MinValue = 0, + MinValue = 0.1f, MaxValue = 1.66f, Default = 1f, Value = 1f, From 2375420d4cbce1372fd2848b7de799bee2f281b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Jan 2022 21:16:10 +0100 Subject: [PATCH 107/587] Tweak allowable ranges of size multiplier --- osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs | 4 ++-- osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs | 4 ++-- osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index 6bd914de33..2d92c925d7 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -19,8 +19,8 @@ namespace osu.Game.Rulesets.Catch.Mods [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] public override BindableNumber SizeMultiplier { get; } = new BindableNumber { - MinValue = 0.4f, - MaxValue = 1.7f, + MinValue = 0.5f, + MaxValue = 1.5f, Default = 1f, Value = 1f, Precision = 0.1f diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index 60de063b24..1ee4ea12e3 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Mania.Mods [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] public override BindableNumber SizeMultiplier { get; } = new BindableNumber { - MinValue = 0.1f, - MaxValue = 4.5f, + MinValue = 0.5f, + MaxValue = 3f, Default = 1f, Value = 1f, Precision = 0.1f diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index bdb30e9ded..fb07c687bb 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Mods [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] public override BindableNumber SizeMultiplier { get; } = new BindableNumber { - MinValue = 0.1f, - MaxValue = 1.66f, + MinValue = 0.5f, + MaxValue = 1.5f, Default = 1f, Value = 1f, Precision = 0.1f From 2d34831b5fd0383c75cf1c4cdc5d49a9b256d892 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 12:54:38 +0900 Subject: [PATCH 108/587] Fix `TestMigration` failing due to changes in realm migration logic Fixes failures as seen at https://github.com/ppy/osu/runs/4927916031?check_suite_focus=true. --- .../NonVisual/CustomDataDirectoryTest.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 61ef31e07e..834930a05e 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -142,19 +142,28 @@ namespace osu.Game.Tests.NonVisual Assert.That(osuStorage, Is.Not.Null); + // In the following tests, realm files are ignored as + // - in the case of checking the source, interacting with the pipe files (client.realm.note) may + // lead to unexpected behaviour. + // - in the case of checking the destination, the files may have already been recreated by the game + // as part of the standard migration flow. + foreach (string file in osuStorage.IgnoreFiles) { - // avoid touching realm files which may be a pipe and break everything. - // this is also done locally inside OsuStorage via the IgnoreFiles list. - if (file.EndsWith(".ini", StringComparison.Ordinal)) + if (!file.Contains("realm", StringComparison.Ordinal)) + { Assert.That(File.Exists(Path.Combine(originalDirectory, file))); - Assert.That(storage.Exists(file), Is.False); + Assert.That(storage.Exists(file), Is.False, () => $"{file} exists in destination when it was expected to be ignored"); + } } foreach (string dir in osuStorage.IgnoreDirectories) { - Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir))); - Assert.That(storage.ExistsDirectory(dir), Is.False); + if (!dir.Contains("realm", StringComparison.Ordinal)) + { + Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir))); + Assert.That(storage.Exists(dir), Is.False, () => $"{dir} exists in destination when it was expected to be ignored"); + } } Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}")); From 3e5c9e843606647561eb35541459386ecf54da20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 12:58:15 +0900 Subject: [PATCH 109/587] Fix cases of `Access` instead of `Realm` --- .../TestSceneOnlinePlayBeatmapAvailabilityTracker.cs | 8 ++++---- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 6 +++--- .../Collections/TestSceneManageCollectionsDialog.cs | 6 +++--- .../Visual/Multiplayer/QueueModeTestScene.cs | 6 +++--- .../Multiplayer/TestSceneDrawableRoomPlaylist.cs | 6 +++--- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 6 +++--- .../TestSceneMultiplayerMatchSongSelect.cs | 6 +++--- .../TestSceneMultiplayerMatchSubScreen.cs | 6 +++--- .../Multiplayer/TestSceneMultiplayerPlaylist.cs | 6 +++--- .../Multiplayer/TestSceneMultiplayerQueueList.cs | 6 +++--- .../Multiplayer/TestSceneMultiplayerReadyButton.cs | 6 +++--- .../TestSceneMultiplayerSpectateButton.cs | 6 +++--- .../Multiplayer/TestScenePlaylistsSongSelect.cs | 6 +++--- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 6 +++--- .../Playlists/TestScenePlaylistsRoomCreation.cs | 6 +++--- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 8 ++++---- .../Visual/SongSelect/TestSceneFilterControl.cs | 6 +++--- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 6 +++--- .../Visual/SongSelect/TestSceneTopLocalRank.cs | 8 ++++---- .../UserInterface/TestSceneDeleteLocalScore.cs | 8 ++++---- osu.Game/Beatmaps/BeatmapModelManager.cs | 4 ++-- osu.Game/Scoring/ScoreModelManager.cs | 2 +- osu.Game/Skinning/SkinModelManager.cs | 2 +- osu.Game/Stores/BeatmapImporter.cs | 2 +- osu.Game/Stores/RealmArchiveModelImporter.cs | 12 ++++++------ osu.Game/Stores/RealmArchiveModelManager.cs | 6 +++--- osu.Game/Tests/Visual/EditorTestScene.cs | 2 +- osu.Game/Tests/Visual/OsuTestScene.cs | 6 +++--- 28 files changed, 82 insertions(+), 82 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index e35e4d9b15..144cbe15c3 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -45,8 +45,8 @@ namespace osu.Game.Tests.Online [BackgroundDependencyLoader] private void load(AudioManager audio, GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.CacheAs(beatmaps = new TestBeatmapManager(LocalStorage, Access, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.CacheAs(beatmaps = new TestBeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API, host)); } @@ -60,8 +60,8 @@ namespace osu.Game.Tests.Online testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile); testBeatmapSet = testBeatmapInfo.BeatmapSet; - Access.Write(r => r.RemoveAll()); - Access.Write(r => r.RemoveAll()); + Realm.Write(r => r.RemoveAll()); + Realm.Write(r => r.RemoveAll()); selectedItem.Value = new PlaylistItem { diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index fbfa7eda6a..40e7c0a844 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -47,10 +47,10 @@ namespace osu.Game.Tests.Visual.Background [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); - Dependencies.Cache(Access); + Dependencies.Cache(Realm); manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index fd0645a1e9..d4c13059da 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.Collections [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Access, rulesets, null, Audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 07b2bdcba3..4cd19b53a4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -47,9 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 0bf1d60ac5..30ac1302aa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -43,9 +43,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 063d886729..3f151a0ae8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -61,9 +61,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, API, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 8c79c468d7..7e3c9722cf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -42,9 +42,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmaps = new List(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 77e2c9c714..9d14d80d07 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -38,9 +38,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 4270818b1a..d970ab6c34 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -33,9 +33,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } [SetUp] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 56e64292c6..d83421ee3a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -38,9 +38,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, API, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index afb60b62aa..9867e5225e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -40,9 +40,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 79b29f0eca..42ae279667 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -41,9 +41,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 6ed57e9899..d7a0885c95 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -34,9 +34,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); var beatmapSet = TestResources.CreateTestBeatmapSetInfo(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index bd95b297d4..73c67d26d9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -42,9 +42,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 716e3a535d..d397b37d05 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -40,9 +40,9 @@ namespace osu.Game.Tests.Visual.Playlists [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, API, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } [SetUpSteps] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 2f5594379b..5e977d075d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -42,10 +42,10 @@ namespace osu.Game.Tests.Visual.SongSelect { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(rulesetStore = new RulesetStore(Access)); - dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Access, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Access, Scheduler)); - Dependencies.Cache(Access); + dependencies.Cache(rulesetStore = new RulesetStore(Realm)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler)); + Dependencies.Cache(Realm); return dependencies; } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 4868a4a075..b384061531 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Access, rulesets, null, Audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index d8a39dda01..0dc4556ed9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -47,9 +47,9 @@ namespace osu.Game.Tests.Visual.SongSelect { // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. // At a point we have isolated interactive test runs enough, this can likely be removed. - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(Access); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(Realm); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); Dependencies.Cache(music = new MusicController()); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs index a0657ffdf6..8e5f76a2eb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs @@ -28,10 +28,10 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Access)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Access, Scheduler)); - Dependencies.Cache(Access); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, Scheduler)); + Dependencies.Cache(Realm); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 2278d3f8bf..94ce85ef38 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -87,10 +87,10 @@ namespace osu.Game.Tests.Visual.UserInterface { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(rulesetStore = new RulesetStore(Access)); - dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Access, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Access, Scheduler)); - Dependencies.Cache(Access); + dependencies.Cache(rulesetStore = new RulesetStore(Realm)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, Scheduler)); + Dependencies.Cache(Realm); var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 167d77d6f6..e8104f2ecb 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -98,12 +98,12 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapInfo? QueryBeatmap(Expression> query) { - return Access.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); + return Realm.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); } public void Update(BeatmapSetInfo item) { - Access.Write(realm => + Realm.Write(realm => { var existing = realm.Find(item.ID); item.CopyChangesToRealm(existing); diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 2147ff1ba1..59102360f9 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -74,7 +74,7 @@ namespace osu.Game.Scoring public override bool IsAvailableLocally(ScoreInfo model) { - return Access.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); + return Realm.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); } } } diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index c93cdb17dd..0af31100a9 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -205,7 +205,7 @@ namespace osu.Game.Skinning private void populateMissingHashes() { - Access.Run(realm => + Realm.Run(realm => { var skinsWithoutHashes = realm.All().Where(i => !i.Protected && string.IsNullOrEmpty(i.Hash)).ToArray(); diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 3d241e795c..a6f20c8d4f 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -165,7 +165,7 @@ namespace osu.Game.Stores public override bool IsAvailableLocally(BeatmapSetInfo model) { - return Access.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); + return Realm.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); } public override string HumanisedModelName => "beatmap"; diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 23a860791e..d9ca3f50a3 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -59,7 +59,7 @@ namespace osu.Game.Stores protected readonly RealmFileStore Files; - protected readonly RealmAccess Access; + protected readonly RealmAccess Realm; /// /// Fired when the user requests to view the resulting import. @@ -73,7 +73,7 @@ namespace osu.Game.Stores protected RealmArchiveModelImporter(Storage storage, RealmAccess realm) { - Access = realm; + Realm = realm; Files = new RealmFileStore(realm, storage); } @@ -320,7 +320,7 @@ namespace osu.Game.Stores /// An optional cancellation token. public virtual Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { - return Access.Run(realm => + return Realm.Run(realm => { cancellationToken.ThrowIfCancellationRequested(); @@ -352,7 +352,7 @@ namespace osu.Game.Stores transaction.Commit(); } - return Task.FromResult((ILive?)existing.ToLive(Access)); + return Task.FromResult((ILive?)existing.ToLive(Realm)); } LogForModel(item, @"Found existing (optimised) but failed pre-check."); @@ -387,7 +387,7 @@ namespace osu.Game.Stores existing.DeletePending = false; transaction.Commit(); - return Task.FromResult((ILive?)existing.ToLive(Access)); + return Task.FromResult((ILive?)existing.ToLive(Realm)); } LogForModel(item, @"Found existing but failed re-use check."); @@ -413,7 +413,7 @@ namespace osu.Game.Stores throw; } - return Task.FromResult((ILive?)item.ToLive(Access)); + return Task.FromResult((ILive?)item.ToLive(Realm)); }); } diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 00cd1c2958..57e51b79aa 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -45,7 +45,7 @@ namespace osu.Game.Stores // This method should be removed as soon as all the surrounding pieces support non-detached operations. if (!item.IsManaged) { - var managed = Access.Realm.Find(item.ID); + var managed = Realm.Realm.Find(item.ID); managed.Realm.Write(() => operation(managed)); item.Files.Clear(); @@ -165,7 +165,7 @@ namespace osu.Game.Stores public bool Delete(TModel item) { - return Access.Run(realm => + return Realm.Run(realm => { if (!item.IsManaged) item = realm.Find(item.ID); @@ -180,7 +180,7 @@ namespace osu.Game.Stores public void Undelete(TModel item) { - Access.Run(realm => + Realm.Run(realm => { if (!item.IsManaged) item = realm.Find(item.ID); diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index a6cac0ec86..6f751b1736 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual working = CreateWorkingBeatmap(Ruleset.Value); if (IsolateSavingFromDatabase) - Dependencies.CacheAs(testBeatmapManager = new TestBeatmapManager(LocalStorage, Access, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.CacheAs(testBeatmapManager = new TestBeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); } protected override void LoadComplete() diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 33dd1d45b8..42e96f80ca 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -75,9 +75,9 @@ namespace osu.Game.Tests.Visual /// /// In interactive runs (ie. VisualTests) this will use the user's database if is not set to true. /// - protected RealmAccess Access => contextFactory.Value; + protected RealmAccess Realm => realm.Value; - private Lazy contextFactory; + private Lazy realm; /// /// Whether a fresh storage should be initialised per test (method) run. @@ -119,7 +119,7 @@ namespace osu.Game.Tests.Visual Resources = parent.Get().Resources; - contextFactory = new Lazy(() => new RealmAccess(LocalStorage, "client")); + realm = new Lazy(() => new RealmAccess(LocalStorage, "client")); RecycleLocalStorage(false); From e23b10e6a5a91ba1d7fd1f746846043c73c0427c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 13:04:05 +0900 Subject: [PATCH 110/587] Update remaining cases of clashing variable name in `realm.Run(realm..` --- osu.Game.Benchmarks/BenchmarkRealmReads.cs | 10 ++-- .../Database/TestRealmKeyBindingStore.cs | 4 +- .../Visual/Ranking/TestSceneResultsScreen.cs | 8 ++-- .../TestSceneDeleteLocalScore.cs | 4 +- osu.Game/Beatmaps/BeatmapManager.cs | 34 +++++++------- osu.Game/Database/EFToRealmMigrator.cs | 46 +++++++++---------- osu.Game/Database/RealmLive.cs | 9 ++-- osu.Game/Input/RealmKeyBindingStore.cs | 10 ++-- .../Settings/Sections/Input/KeyBindingRow.cs | 6 +-- osu.Game/Scoring/ScoreManager.cs | 8 ++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 10 ++-- osu.Game/Skinning/SkinManager.cs | 6 +-- 13 files changed, 77 insertions(+), 80 deletions(-) diff --git a/osu.Game.Benchmarks/BenchmarkRealmReads.cs b/osu.Game.Benchmarks/BenchmarkRealmReads.cs index 412f86bd1c..bf9467700c 100644 --- a/osu.Game.Benchmarks/BenchmarkRealmReads.cs +++ b/osu.Game.Benchmarks/BenchmarkRealmReads.cs @@ -29,7 +29,7 @@ namespace osu.Game.Benchmarks realm = new RealmAccess(storage, "client"); - realm.Run(realm => + realm.Run(r => { realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo }))); }); @@ -41,9 +41,9 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkDirectPropertyRead() { - realm.Run(realm => + realm.Run(r => { - var beatmapSet = realm.All().First(); + var beatmapSet = r.All().First(); for (int i = 0; i < ReadsPerFetch; i++) { @@ -119,9 +119,9 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkDetachedPropertyRead() { - realm.Run(realm => + realm.Run(r => { - var beatmapSet = realm.All().First().Detach(); + var beatmapSet = r.All().First().Detach(); for (int i = 0; i < ReadsPerFetch; i++) { diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 4b8816f142..7b2d2a1278 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -76,9 +76,9 @@ namespace osu.Game.Tests.Database private int queryCount(GlobalAction? match = null) { - return realm.Run(realm => + return realm.Run(r => { - var results = realm.All(); + var results = r.All(); if (match.HasValue) results = results.Where(k => k.ActionInt == (int)match.Value); return results.Count(); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 13b4af5223..988f429ff5 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -42,11 +42,11 @@ namespace osu.Game.Tests.Visual.Ranking { base.LoadComplete(); - realm.Run(realm => + realm.Run(r => { - var beatmapInfo = realm.All() - .Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0) - .FirstOrDefault(); + var beatmapInfo = r.All() + .Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0) + .FirstOrDefault(); if (beatmapInfo != null) Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 94ce85ef38..55031a9699 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -122,10 +122,10 @@ namespace osu.Game.Tests.Visual.UserInterface [SetUp] public void Setup() => Schedule(() => { - realm.Run(realm => + realm.Run(r => { // Due to soft deletions, we can re-use deleted scores between test runs - scoreManager.Undelete(realm.All().Where(s => s.DeletePending).ToList()); + scoreManager.Undelete(r.All().Where(s => s.DeletePending).ToList()); }); leaderboard.Scores = null; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index ddc1d054cc..4488301fe5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -119,12 +119,12 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to hide. public void Hide(BeatmapInfo beatmapInfo) { - realm.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.BeginWrite()) { if (!beatmapInfo.IsManaged) - beatmapInfo = realm.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID); beatmapInfo.Hidden = true; transaction.Commit(); @@ -138,12 +138,12 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to restore. public void Restore(BeatmapInfo beatmapInfo) { - realm.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.BeginWrite()) { if (!beatmapInfo.IsManaged) - beatmapInfo = realm.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID); beatmapInfo.Hidden = false; transaction.Commit(); @@ -153,11 +153,11 @@ namespace osu.Game.Beatmaps public void RestoreAll() { - realm.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.BeginWrite()) { - foreach (var beatmap in realm.All().Where(b => b.Hidden)) + foreach (var beatmap in r.All().Where(b => b.Hidden)) beatmap.Hidden = false; transaction.Commit(); @@ -171,10 +171,10 @@ namespace osu.Game.Beatmaps /// A list of available . public List GetAllUsableBeatmapSets() { - return realm.Run(realm => + return realm.Run(r => { - realm.Refresh(); - return realm.All().Where(b => !b.DeletePending).Detach(); + r.Refresh(); + return r.All().Where(b => !b.DeletePending).Detach(); }); } @@ -240,9 +240,9 @@ namespace osu.Game.Beatmaps public void Delete(Expression>? filter = null, bool silent = false) { - realm.Run(realm => + realm.Run(r => { - var items = realm.All().Where(s => !s.DeletePending && !s.Protected); + var items = r.All().Where(s => !s.DeletePending && !s.Protected); if (filter != null) items = items.Where(filter); @@ -253,7 +253,7 @@ namespace osu.Game.Beatmaps public void UndeleteAll() { - realm.Run(realm => beatmapModelManager.Undelete(realm.All().Where(s => s.DeletePending).ToList())); + realm.Run(r => beatmapModelManager.Undelete(r.All().Where(s => s.DeletePending).ToList())); } public void Undelete(List items, bool silent = false) @@ -312,9 +312,9 @@ namespace osu.Game.Beatmaps // If we seem to be missing files, now is a good time to re-fetch. if (importedBeatmap?.BeatmapSet?.Files.Count == 0) { - realm.Run(realm => + realm.Run(r => { - var refetch = realm.Find(importedBeatmap.ID)?.Detach(); + var refetch = r.Find(importedBeatmap.ID)?.Detach(); if (refetch != null) importedBeatmap = refetch; diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index a0787e81e6..349452afce 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -158,11 +158,11 @@ namespace osu.Game.Database int count = existingBeatmapSets.Count(); - realm.Run(realm => + realm.Run(r => { log($"Found {count} beatmaps in EF"); - var transaction = realm.BeginWrite(); + var transaction = r.BeginWrite(); int written = 0; try @@ -172,7 +172,7 @@ namespace osu.Game.Database if (++written % 1000 == 0) { transaction.Commit(); - transaction = realm.BeginWrite(); + transaction = r.BeginWrite(); log($"Migrated {written}/{count} beatmaps..."); } @@ -186,11 +186,11 @@ namespace osu.Game.Database Protected = beatmapSet.Protected, }; - migrateFiles(beatmapSet, realm, realmBeatmapSet); + migrateFiles(beatmapSet, r, realmBeatmapSet); foreach (var beatmap in beatmapSet.Beatmaps) { - var ruleset = realm.Find(beatmap.RulesetInfo.ShortName); + var ruleset = r.Find(beatmap.RulesetInfo.ShortName); var metadata = getBestMetadata(beatmap.Metadata, beatmapSet.Metadata); var realmBeatmap = new BeatmapInfo(ruleset, new BeatmapDifficulty(beatmap.BaseDifficulty), metadata) @@ -225,7 +225,7 @@ namespace osu.Game.Database realmBeatmapSet.Beatmaps.Add(realmBeatmap); } - realm.Add(realmBeatmapSet); + r.Add(realmBeatmapSet); } } finally @@ -280,11 +280,11 @@ namespace osu.Game.Database int count = existingScores.Count(); - realm.Run(realm => + realm.Run(r => { log($"Found {count} scores in EF"); - var transaction = realm.BeginWrite(); + var transaction = r.BeginWrite(); int written = 0; try @@ -294,12 +294,12 @@ namespace osu.Game.Database if (++written % 1000 == 0) { transaction.Commit(); - transaction = realm.BeginWrite(); + transaction = r.BeginWrite(); log($"Migrated {written}/{count} scores..."); } - var beatmap = realm.All().First(b => b.Hash == score.BeatmapInfo.Hash); - var ruleset = realm.Find(score.Ruleset.ShortName); + var beatmap = r.All().First(b => b.Hash == score.BeatmapInfo.Hash); + var ruleset = r.Find(score.Ruleset.ShortName); var user = new RealmUser { OnlineID = score.User.OnlineID, @@ -329,9 +329,9 @@ namespace osu.Game.Database APIMods = score.APIMods, }; - migrateFiles(score, realm, realmScore); + migrateFiles(score, r, realmScore); - realm.Add(realmScore); + r.Add(realmScore); } } finally @@ -369,13 +369,13 @@ namespace osu.Game.Database break; } - realm.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.BeginWrite()) { // only migrate data if the realm database is empty. - // note that this cannot be written as: `realm.All().All(s => s.Protected)`, because realm does not support `.All()`. - if (!realm.All().Any(s => !s.Protected)) + // note that this cannot be written as: `r.All().All(s => s.Protected)`, because realm does not support `.All()`. + if (!r.All().Any(s => !s.Protected)) { log($"Migrating {existingSkins.Count} skins"); @@ -390,9 +390,9 @@ namespace osu.Game.Database InstantiationInfo = skin.InstantiationInfo, }; - migrateFiles(skin, realm, realmSkin); + migrateFiles(skin, r, realmSkin); - realm.Add(realmSkin); + r.Add(realmSkin); if (skin.ID == userSkinInt) userSkinChoice.Value = realmSkin.ID.ToString(); @@ -428,12 +428,12 @@ namespace osu.Game.Database log("Beginning settings migration to realm"); - realm.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.BeginWrite()) { // only migrate data if the realm database is empty. - if (!realm.All().Any()) + if (!r.All().Any()) { log($"Migrating {existingSettings.Count} settings"); @@ -447,7 +447,7 @@ namespace osu.Game.Database if (string.IsNullOrEmpty(shortName)) continue; - realm.Add(new RealmRulesetSetting + r.Add(new RealmRulesetSetting { Key = dkb.Key, Value = dkb.StringValue, diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 29159fd5be..f06ba1eff9 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -51,10 +51,7 @@ namespace osu.Game.Database return; } - realm.Run(realm => - { - perform(retrieveFromID(realm, ID)); - }); + realm.Run(r => perform(retrieveFromID(r, ID))); } /// @@ -66,9 +63,9 @@ namespace osu.Game.Database if (!IsManaged) return perform(data); - return realm.Run(realm => + return realm.Run(r => { - var returnData = perform(retrieveFromID(realm, ID)); + var returnData = perform(retrieveFromID(r, ID)); if (returnData is RealmObjectBase realmObject && realmObject.IsManaged) throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}."); diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index cccd42a9aa..20971ffca5 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -56,21 +56,21 @@ namespace osu.Game.Input /// The rulesets to populate defaults from. public void Register(KeyBindingContainer container, IEnumerable rulesets) { - realm.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.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 = realm.All().ToList(); + var existingBindings = r.All().ToList(); - insertDefaults(realm, existingBindings, container.DefaultKeyBindings); + insertDefaults(r, existingBindings, container.DefaultKeyBindings); foreach (var ruleset in rulesets) { var instance = ruleset.CreateInstance(); foreach (int variant in instance.AvailableVariants) - insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ShortName, variant); + insertDefaults(r, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ShortName, variant); } transaction.Commit(); diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 91883e4f41..2405618917 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -386,10 +386,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void updateStoreFromButton(KeyButton button) { - realm.Run(realm => + realm.Run(r => { - var binding = realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); - realm.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString); + var binding = r.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); + r.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString); }); } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index e712d170cd..bb26bd3b04 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -51,7 +51,7 @@ namespace osu.Game.Scoring /// The first result for the provided query, or null if no results were found. public ScoreInfo Query(Expression> query) { - return realm.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); + return realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); } /// @@ -254,10 +254,10 @@ namespace osu.Game.Scoring public void Delete([CanBeNull] Expression> filter = null, bool silent = false) { - realm.Run(realm => + realm.Run(r => { - var items = realm.All() - .Where(s => !s.DeletePending); + var items = r.All() + .Where(s => !s.DeletePending); if (filter != null) items = items.Where(filter); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 8e0fdea0f8..b02abd2f7a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -179,7 +179,7 @@ namespace osu.Game.Screens.Select if (!loadedTestBeatmaps) { - realm.Run(realm => loadBeatmapSets(getBeatmapSets(realm))); + realm.Run(r => loadBeatmapSets(getBeatmapSets(r))); } } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 3d262f8b97..b53aa6f43c 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -150,12 +150,12 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { - realm.Run(realm => + realm.Run(r => { - var scores = realm.All() - .AsEnumerable() - // TODO: update to use a realm filter directly (or at least figure out the beatmap part to reduce scope). - .Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.OnlineID == ruleset.Value.ID); + var scores = r.All() + .AsEnumerable() + // TODO: update to use a realm filter directly (or at least figure out the beatmap part to reduce scope). + .Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.OnlineID == ruleset.Value.ID); if (filterMods && !mods.Value.Any()) { diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 66956325da..22fb8ce86f 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -289,10 +289,10 @@ namespace osu.Game.Skinning public void Delete([CanBeNull] Expression> filter = null, bool silent = false) { - realm.Run(realm => + realm.Run(r => { - var items = realm.All() - .Where(s => !s.Protected && !s.DeletePending); + var items = r.All() + .Where(s => !s.Protected && !s.DeletePending); if (filter != null) items = items.Where(filter); From d7342880f5a099e3260695bd04741a61864ff921 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 13:09:47 +0900 Subject: [PATCH 111/587] Update remaining cases of clashes with `realm.Write` and `realm.RegisterForNotifications` --- .../Database/TestRealmKeyBindingStore.cs | 8 ++++---- osu.Game/Database/EFToRealmMigrator.cs | 10 +++++----- .../Input/Bindings/DatabasedKeyBindingContainer.cs | 2 +- osu.Game/Online/BeatmapDownloadTracker.cs | 2 +- .../Rooms/OnlinePlayBeatmapAvailabilityTracker.cs | 2 +- osu.Game/Online/ScoreDownloadTracker.cs | 2 +- osu.Game/Overlays/MusicController.cs | 2 +- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 2 +- .../Rulesets/Configuration/RulesetConfigManager.cs | 4 ++-- osu.Game/Rulesets/RulesetStore.cs | 6 +++--- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +++--- osu.Game/Screens/Select/Carousel/TopLocalRank.cs | 14 +++++++------- .../Select/Leaderboards/BeatmapLeaderboard.cs | 6 +++--- osu.Game/Skinning/SkinManager.cs | 6 +++--- osu.Game/Stores/RealmFileStore.cs | 6 +++--- 15 files changed, 39 insertions(+), 39 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 7b2d2a1278..891801865f 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -60,11 +60,11 @@ namespace osu.Game.Tests.Database KeyBindingContainer testContainer = new TestKeyBindingContainer(); // Add some excess bindings for an action which only supports 1. - realm.Write(realm => + realm.Write(r => { - realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.A))); - realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.S))); - realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.D))); + r.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.A))); + r.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.S))); + r.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.D))); }); Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3)); diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 349452afce..adf91e4a41 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -101,15 +101,15 @@ namespace osu.Game.Database { using (var ef = efContextFactory.Get()) { - realm.Write(realm => + realm.Write(r => { // Before beginning, ensure realm is in an empty state. // Migrations which are half-completed could lead to issues if the user tries a second time. // Note that we only do this for beatmaps and scores since the other migrations are yonks old. - realm.RemoveAll(); - realm.RemoveAll(); - realm.RemoveAll(); - realm.RemoveAll(); + r.RemoveAll(); + r.RemoveAll(); + r.RemoveAll(); + r.RemoveAll(); }); migrateSettings(ef); diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index d54c049c99..3e4a9759a3 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -55,7 +55,7 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { - realmSubscription = realm.RegisterForNotifications(realm => queryRealmKeyBindings(), (sender, changes, error) => + realmSubscription = realm.RegisterForNotifications(r => queryRealmKeyBindings(), (sender, changes, error) => { // The first fire of this is a bit redundant as this is being called in base.LoadComplete, // but this is safest in case the subscription is restored after a context recycle. diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index f54dc30620..9f795f007a 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realm.RegisterForNotifications(realm => realm.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index c562695ac1..c67cbade6a 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -78,7 +78,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(realm => filteredBeatmaps(), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => filteredBeatmaps(), (items, changes, ___) => { if (changes == null) return; diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index 81dfc811a4..d7e31c8a59 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; - realmSubscription = realm.RegisterForNotifications(realm => realm.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending), (items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 0f8e390ebf..9dc55d24dd 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -79,7 +79,7 @@ namespace osu.Game.Overlays protected override void LoadComplete() { base.LoadComplete(); - beatmapSubscription = realm.RegisterForNotifications(realm => queryRealmBeatmapSets(), beatmapsChanged); + beatmapSubscription = realm.RegisterForNotifications(r => queryRealmBeatmapSets(), beatmapsChanged); } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index af3fd5c9bf..8ab296c0a8 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -83,7 +83,7 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Current = dropdownBindable; - realmSubscription = realm.RegisterForNotifications(realm => queryRealmSkins(), (sender, changes, error) => + realmSubscription = realm.RegisterForNotifications(r => queryRealmSkins(), (sender, changes, error) => { // The first fire of this is a bit redundant due to the call below, // but this is safest in case the subscription is restored after a context recycle. diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index cfa20e0b87..30bb95ba72 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -56,11 +56,11 @@ namespace osu.Game.Rulesets.Configuration pendingWrites.Clear(); } - realm?.Write(realm => + realm?.Write(r => { foreach (var c in changed) { - var setting = realm.All().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString()); + var setting = r.All().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString()); setting.Value = ConfigStore[c].ToString(); } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 606bc65599..9af9ace7ad 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets { public class RulesetStore : IDisposable, IRulesetStore { - private readonly RealmAccess realm; + private readonly RealmAccess realmAccess; private const string ruleset_library_prefix = @"osu.Game.Rulesets"; @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets public RulesetStore(RealmAccess realm, Storage? storage = null) { - this.realm = realm; + realmAccess = realm; // On android in release configuration assemblies are loaded from the apk directly into memory. // We cannot read assemblies from cwd, so should check loaded assemblies instead. @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets private void addMissingRulesets() { - realm.Write(realm => + realmAccess.Write(realm => { var rulesets = realm.All(); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index b02abd2f7a..dff2c598c3 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -191,12 +191,12 @@ namespace osu.Game.Screens.Select base.LoadComplete(); subscriptionSets = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); - subscriptionBeatmaps = realm.RegisterForNotifications(realm => realm.All().Where(b => !b.Hidden), beatmapsChanged); + subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => !b.Hidden), beatmapsChanged); // Can't use main subscriptions because we can't lookup deleted indices. // https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595. - subscriptionDeletedSets = realm.RegisterForNotifications(realm => realm.All().Where(s => s.DeletePending && !s.Protected), deletedBeatmapSetsChanged); - subscriptionHiddenBeatmaps = realm.RegisterForNotifications(realm => realm.All().Where(b => b.Hidden), beatmapsChanged); + subscriptionDeletedSets = realm.RegisterForNotifications(r => r.All().Where(s => s.DeletePending && !s.Protected), deletedBeatmapSetsChanged); + subscriptionHiddenBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => b.Hidden), beatmapsChanged); } private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 021dfd06f7..e1f9c1b508 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -48,13 +48,13 @@ namespace osu.Game.Screens.Select.Carousel ruleset.BindValueChanged(_ => { scoreSubscription?.Dispose(); - scoreSubscription = realm.RegisterForNotifications(realm => - realm.All() - .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" - + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" - + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName) - .OrderByDescending(s => s.TotalScore), + scoreSubscription = realm.RegisterForNotifications(r => + r.All() + .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" + + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" + + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName) + .OrderByDescending(s => s.TotalScore), (items, changes, ___) => { Rank = items.FirstOrDefault()?.Rank; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index b53aa6f43c..f25997650b 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -113,9 +113,9 @@ namespace osu.Game.Screens.Select.Leaderboards if (beatmapInfo == null) return; - scoreSubscription = realm.RegisterForNotifications(realm => - realm.All() - .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID), + scoreSubscription = realm.RegisterForNotifications(r => + r.All() + .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID), (_, changes, ___) => { if (!IsOnlineScope) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 22fb8ce86f..344acb7a0f 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -87,12 +87,12 @@ namespace osu.Game.Skinning }; // Ensure the default entries are present. - realm.Write(realm => + realm.Write(r => { foreach (var skin in defaultSkins) { - if (realm.Find(skin.SkinInfo.ID) == null) - realm.Add(skin.SkinInfo.Value); + if (r.Find(skin.SkinInfo.ID) == null) + r.Add(skin.SkinInfo.Value); } }); diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs index 5edc1be954..b5dd3d64e4 100644 --- a/osu.Game/Stores/RealmFileStore.cs +++ b/osu.Game/Stores/RealmFileStore.cs @@ -92,10 +92,10 @@ namespace osu.Game.Stores int removedFiles = 0; // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. - realm.Write(realm => + realm.Write(r => { // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707) - var files = realm.All().ToList(); + var files = r.All().ToList(); foreach (var file in files) { @@ -108,7 +108,7 @@ namespace osu.Game.Stores { removedFiles++; Storage.Delete(file.GetStoragePath()); - realm.Remove(file); + r.Remove(file); } catch (Exception e) { From bbcc149e2e7cc01045374b7b25be85983658aa0e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 13:42:41 +0900 Subject: [PATCH 112/587] During import if files are found to be missing, ensure they are restored This is one step closer to sanity in terms of physical files. As per the comment I have left in place, we really should be checking file sizes or hashes, but to keep things simple and fast I've opted to just cover the "missing file" scenario for now. Ran into this when testing against a foreign `client.realm` by: - Noticing a beatmap doesn't load - Deleting said beatmap - Downloading via beatmap overlay - Beatmap is restored but still doesn't work Note that I've kept the logic where this will undelete an existing import rather than create one from fresh, as I think that is beneficial to the user (ie. it will still keep any linked scores on restore). --- .../Database/BeatmapImporterTests.cs | 32 +++++++++++++++++++ osu.Game/Stores/RealmArchiveModelImporter.cs | 10 ++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 227314cffd..abab8ae3c6 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -600,6 +600,38 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestImportThenReimportAfterMissingFiles() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapModelManager(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); + + var imported = await LoadOszIntoStore(importer, realmFactory.Context); + + deleteBeatmapSet(imported, realmFactory.Context); + + Assert.IsTrue(imported.DeletePending); + + // intentionally nuke all files + storage.DeleteDirectory("files"); + + Assert.That(imported.Files.All(f => !storage.GetStorageForDirectory("files").Exists(f.File.GetStoragePath()))); + + var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + Assert.IsFalse(imported.DeletePending); + Assert.IsFalse(importedSecondTime.DeletePending); + + // check that the files now exist, even though they were deleted above. + Assert.That(importedSecondTime.Files.All(f => storage.GetStorageForDirectory("files").Exists(f.File.GetStoragePath()))); + }); + } + [Test] public void TestImportThenDeleteThenImportNonOptimisedPath() { diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 3d8e9f2703..154135ea71 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -342,7 +342,8 @@ namespace osu.Game.Stores // note that this should really be checking filesizes on disk (of existing files) for some degree of sanity. // or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files. if (CanSkipImport(existing, item) && - getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f))) + getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f)) && + checkAllFilesExist(existing)) { LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); @@ -459,7 +460,6 @@ namespace osu.Game.Stores if (!(prefix.EndsWith('/') || prefix.EndsWith('\\'))) prefix = string.Empty; - // import files to manager foreach (string file in reader.Filenames) yield return (file, file.Substring(prefix.Length).ToStandardisedPath()); } @@ -519,7 +519,11 @@ namespace osu.Game.Stores // for the best or worst, we copy and import files of a new import before checking whether // it is a duplicate. so to check if anything has changed, we can just compare all File IDs. getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) && - getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files)); + getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files)) && + checkAllFilesExist(existing); + + private bool checkAllFilesExist(TModel model) => + model.Files.All(f => Files.Storage.Exists(f.File.GetStoragePath())); /// /// Whether this specified path should be removed after successful import. From df1297ade6fcdf95b9025252aae63cbebcd0ba0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 13:50:39 +0900 Subject: [PATCH 113/587] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index b296c114e9..4e5b9fdbb1 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 758575e74a..af5d8a5920 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 5925581e28..2bcdea61b3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From dd2caea694f0e2782c4c34b8dba3c165ef9136da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 19:10:14 +0900 Subject: [PATCH 114/587] Update `GetSuitableHost` usages in line with new `HostOptions` --- osu.Desktop/Program.cs | 2 +- osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs | 3 ++- .../NonVisual/CustomTourneyDirectoryTest.cs | 5 +++-- osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs | 3 ++- osu.Game.Tournament.Tests/TournamentTestRunner.cs | 2 +- osu.Game/Tests/CleanRunHeadlessGameHost.cs | 6 +++++- osu.Game/Tests/VisualTestRunner.cs | 2 +- 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 7ec7d53a7e..ab9935a220 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -55,7 +55,7 @@ namespace osu.Desktop } } - using (DesktopGameHost host = Host.GetSuitableHost(gameName, true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { PortableInstallation = true })) { host.ExceptionThrown += handleException; diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index 53e4ef07e7..ee746ba4d5 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -6,6 +6,7 @@ using System.IO; using System.Text; using System.Threading.Tasks; using NUnit.Framework; +using osu.Framework; using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Testing; @@ -155,7 +156,7 @@ namespace osu.Game.Tests.Collections.IO } // Name matches the automatically chosen name from `CleanRunHeadlessGameHost` above, so we end up using the same storage location. - using (HeadlessGameHost host = new TestRunHeadlessGameHost(firstRunName)) + using (HeadlessGameHost host = new TestRunHeadlessGameHost(firstRunName, null)) { try { diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index fc5d3b652f..36d0c6f0f6 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using NUnit.Framework; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; using osu.Framework.Testing; @@ -37,7 +38,7 @@ namespace osu.Game.Tournament.Tests.NonVisual [Test] public void TestCustomDirectory() { - using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestCustomDirectory))) // don't use clean run as we are writing a config file. + using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestCustomDirectory), null)) // don't use clean run as we are writing a config file. { string osuDesktopStorage = Path.Combine(host.UserStoragePaths.First(), nameof(TestCustomDirectory)); const string custom_tournament = "custom"; @@ -68,7 +69,7 @@ namespace osu.Game.Tournament.Tests.NonVisual [Test] public void TestMigration() { - using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestMigration))) // don't use clean run as we are writing test files for migration. + using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestMigration), null)) // don't use clean run as we are writing test files for migration. { string osuRoot = Path.Combine(host.UserStoragePaths.First(), nameof(TestMigration)); string configFile = Path.Combine(osuRoot, "tournament.ini"); diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs index 03252e3be6..11b686e3e8 100644 --- a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using NUnit.Framework; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; using osu.Framework.Testing; @@ -19,7 +20,7 @@ namespace osu.Game.Tournament.Tests.NonVisual public void CheckIPCLocation() { // don't use clean run because files are being written before osu! launches. - using (var host = new TestRunHeadlessGameHost(nameof(CheckIPCLocation))) + using (var host = new TestRunHeadlessGameHost(nameof(CheckIPCLocation), null)) { string basePath = Path.Combine(host.UserStoragePaths.First(), nameof(CheckIPCLocation)); diff --git a/osu.Game.Tournament.Tests/TournamentTestRunner.cs b/osu.Game.Tournament.Tests/TournamentTestRunner.cs index 1f63f7c545..23da37a6b7 100644 --- a/osu.Game.Tournament.Tests/TournamentTestRunner.cs +++ b/osu.Game.Tournament.Tests/TournamentTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tournament.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { PortableInstallation = true })) { host.Run(new TournamentTestBrowser()); return 0; diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs index 754c9044e8..bdb171c528 100644 --- a/osu.Game/Tests/CleanRunHeadlessGameHost.cs +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using osu.Framework; using osu.Framework.Testing; namespace osu.Game.Tests @@ -20,7 +21,10 @@ namespace osu.Game.Tests /// Whether to bypass directory cleanup on host disposal. Should be used only if a subsequent test relies on the files still existing. /// The name of the calling method, used for test file isolation and clean-up. public CleanRunHeadlessGameHost(bool bindIPC = false, bool realtime = true, bool bypassCleanup = false, [CallerMemberName] string callingMethodName = @"") - : base($"{callingMethodName}-{Guid.NewGuid()}", bindIPC, realtime, bypassCleanup: bypassCleanup) + : base($"{callingMethodName}-{Guid.NewGuid()}", new HostOptions + { + BindIPC = bindIPC, + }, bypassCleanup: bypassCleanup, realtime: realtime) { } diff --git a/osu.Game/Tests/VisualTestRunner.cs b/osu.Game/Tests/VisualTestRunner.cs index d63b3d48b2..973801099e 100644 --- a/osu.Game/Tests/VisualTestRunner.cs +++ b/osu.Game/Tests/VisualTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { PortableInstallation = true, })) { host.Run(new OsuTestBrowser()); return 0; From a5c76a9647c56b1677bd9105c93220ee7ad59441 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 13:56:47 +0900 Subject: [PATCH 115/587] Fix a few more cases of "context" terminology usage --- osu.Game/Database/RealmAccess.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 66b4edbe84..65e13cf542 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -30,7 +30,7 @@ using Realms.Exceptions; namespace osu.Game.Database { /// - /// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage. + /// A factory which provides safe access to the realm storage backend. /// public class RealmAccess : IDisposable { @@ -57,9 +57,9 @@ namespace osu.Game.Database private const int schema_version = 13; /// - /// Lock object which is held during sections, blocking context creation during blocking periods. + /// Lock object which is held during sections, blocking realm retrieval during blocking periods. /// - private readonly SemaphoreSlim realmCreationLock = new SemaphoreSlim(1); + private readonly SemaphoreSlim realmRetrievalLock = new SemaphoreSlim(1); private readonly ThreadLocal currentThreadCanCreateRealmInstances = new ThreadLocal(); @@ -76,7 +76,7 @@ namespace osu.Game.Database /// /// Holds a map of functions registered via and a coinciding action which when triggered, - /// fires a change set event with an empty collection. This is used to inform subscribers when a realm context goes away, and ensure they don't use invalidated + /// fires a change set event with an empty collection. This is used to inform subscribers when the main realm instance gets recycled, and ensure they don't use invalidated /// managed realm objects from a previous firing. /// private readonly Dictionary, Action> notificationsResetMap = new Dictionary, Action>(); @@ -364,7 +364,7 @@ namespace osu.Game.Database { if (!currentThreadCanCreateRealmInstances.Value) { - realmCreationLock.Wait(); + realmRetrievalLock.Wait(); currentThreadCanCreateRealmInstances.Value = true; tookSemaphoreLock = true; } @@ -383,7 +383,7 @@ namespace osu.Game.Database { if (tookSemaphoreLock) { - realmCreationLock.Release(); + realmRetrievalLock.Release(); currentThreadCanCreateRealmInstances.Value = false; } } @@ -589,7 +589,7 @@ namespace osu.Game.Database try { - realmCreationLock.Wait(); + realmRetrievalLock.Wait(); lock (realmLock) { @@ -639,13 +639,13 @@ namespace osu.Game.Database } catch { - realmCreationLock.Release(); + realmRetrievalLock.Release(); throw; } return new InvokeOnDisposal(this, factory => { - factory.realmCreationLock.Release(); + factory.realmRetrievalLock.Release(); Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); // Post back to the update thread to revive any subscriptions. @@ -667,9 +667,9 @@ namespace osu.Game.Database if (!isDisposed) { - // intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal. - realmCreationLock.Wait(); - realmCreationLock.Dispose(); + // intentionally block realm retrieval indefinitely. this ensures that nothing can start consuming a new instance after disposal. + realmRetrievalLock.Wait(); + realmRetrievalLock.Dispose(); isDisposed = true; } From 8116806db3fa6b06e96d1f38eac77964a96435a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 14:00:58 +0900 Subject: [PATCH 116/587] Add test coverage of calling `BlockAllOperations` a second time after timeout --- osu.Game.Tests/Database/GeneralUsageTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 2533c832e6..8262ef18d4 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -95,6 +95,11 @@ namespace osu.Game.Tests.Database }); stopThreadedUsage.Set(); + + // Ensure we can block a second time after the usage has ended. + using (realm.BlockAllOperations()) + { + } }); } } From 86c844bd58317c2fc5d978a6142ea3ac153794a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 14:15:29 +0900 Subject: [PATCH 117/587] Update remaining usages of `GetSuitableHost` in template projects --- .../osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs | 2 +- .../osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs | 2 +- .../osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs | 2 +- .../osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs | 2 +- osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs | 1 - .../NonVisual/CustomTourneyDirectoryTest.cs | 1 - osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs | 1 - 7 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs index 4f810ce17f..03ee7c9204 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs index fd6bd9b714..55c0cf6a3b 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs index 65cfb2bff4..b45505678c 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs index fd6bd9b714..55c0cf6a3b 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) { host.Run(new OsuTestBrowser()); return 0; diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index ee746ba4d5..5cbede54f5 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -6,7 +6,6 @@ using System.IO; using System.Text; using System.Threading.Tasks; using NUnit.Framework; -using osu.Framework; using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Testing; diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 36d0c6f0f6..26fb03bed4 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using NUnit.Framework; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; using osu.Framework.Testing; diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs index 11b686e3e8..80cc9be5c1 100644 --- a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using NUnit.Framework; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; using osu.Framework.Testing; From 5872dabf604f55a1feec63039404780a9d48133b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 14:16:15 +0900 Subject: [PATCH 118/587] Fix incorrect flag to options conversion --- osu.Desktop/Program.cs | 2 +- osu.Game.Tournament.Tests/TournamentTestRunner.cs | 2 +- osu.Game/Tests/VisualTestRunner.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index ab9935a220..b944068e78 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -55,7 +55,7 @@ namespace osu.Desktop } } - using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { PortableInstallation = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true })) { host.ExceptionThrown += handleException; diff --git a/osu.Game.Tournament.Tests/TournamentTestRunner.cs b/osu.Game.Tournament.Tests/TournamentTestRunner.cs index 23da37a6b7..229ab41a1e 100644 --- a/osu.Game.Tournament.Tests/TournamentTestRunner.cs +++ b/osu.Game.Tournament.Tests/TournamentTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tournament.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { PortableInstallation = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) { host.Run(new TournamentTestBrowser()); return 0; diff --git a/osu.Game/Tests/VisualTestRunner.cs b/osu.Game/Tests/VisualTestRunner.cs index 973801099e..6aa75ec147 100644 --- a/osu.Game/Tests/VisualTestRunner.cs +++ b/osu.Game/Tests/VisualTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { PortableInstallation = true, })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true, })) { host.Run(new OsuTestBrowser()); return 0; From 56d7d814658d1d202e405f237cf74d8a386d078b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 14:47:21 +0900 Subject: [PATCH 119/587] Fix broken test due to `SynchronizationContext` never running expected work --- osu.Game.Tests/Database/GeneralUsageTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 8262ef18d4..dc0d42595b 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -87,6 +87,11 @@ namespace osu.Game.Tests.Database hasThreadedUsage.Wait(); + // Usually the host would run the synchronization context work per frame. + // For the sake of keeping this test simple (there's only one update invocation), + // let's replace it so we can ensure work is run immediately. + SynchronizationContext.SetSynchronizationContext(new ImmediateExecuteSynchronizationContext()); + Assert.Throws(() => { using (realm.BlockAllOperations()) @@ -102,5 +107,10 @@ namespace osu.Game.Tests.Database } }); } + + private class ImmediateExecuteSynchronizationContext : SynchronizationContext + { + public override void Post(SendOrPostCallback d, object? state) => d(state); + } } } From 5fb9b58c9b12c2929d07eaccfabf71528f259fe6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jan 2022 20:23:10 +0900 Subject: [PATCH 120/587] Add tracking of total subscriptions --- osu.Game/Database/RealmAccess.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 65e13cf542..fe4c30d7dd 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -83,6 +83,8 @@ namespace osu.Game.Database private static readonly GlobalStatistic realm_instances_created = GlobalStatistics.Get(@"Realm", @"Instances (Created)"); + private static readonly GlobalStatistic total_subscriptions = GlobalStatistics.Get(@"Realm", @"Subscriptions"); + private readonly object realmLock = new object(); private Realm? updateRealm; @@ -289,6 +291,8 @@ namespace osu.Game.Database var syncContext = SynchronizationContext.Current; + total_subscriptions.Value++; + registerSubscription(action); // This token is returned to the consumer. @@ -309,6 +313,7 @@ namespace osu.Game.Database unsubscriptionAction?.Dispose(); customSubscriptionsResetMap.Remove(action); notificationsResetMap.Remove(action); + total_subscriptions.Value--; } } } From ae0fea8e2627f68f211c89a27a9bae04a651b0f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 15:29:45 +0900 Subject: [PATCH 121/587] Fix compilation issues due to misnamed fild --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 8cb762ed12..b37d2be0bc 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -608,9 +608,9 @@ namespace osu.Game.Tests.Database using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realmFactory.Realm); - deleteBeatmapSet(imported, realmFactory.Context); + deleteBeatmapSet(imported, realmFactory.Realm); Assert.IsTrue(imported.DeletePending); @@ -619,7 +619,7 @@ namespace osu.Game.Tests.Database Assert.That(imported.Files.All(f => !storage.GetStorageForDirectory("files").Exists(f.File.GetStoragePath()))); - var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Realm); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID == importedSecondTime.ID); From 778d2a71b484bb3220a09db15fc5fc632cb891e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 15:23:51 +0900 Subject: [PATCH 122/587] Remove `Task` from the inner-most `Import` method in `RealmArchiveModelImporter` One of my pending work items for post-realm merge. The lowest-level import task is no longer asynchronous, as we don't want it to span multiple threads to allow easier interaction with realm. Removing the `Task` spec simplifies a heap of usages. Individual usages should decide whether they want to run the import asynchronously, by either using an alternative override or spooling up a thread themselves. --- .../Database/BeatmapImporterTests.cs | 4 ++-- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 10 ++++----- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 22 +++++++++---------- .../Gameplay/TestSceneReplayDownloadButton.cs | 2 +- .../Menus/TestSceneMusicActionHandling.cs | 2 +- .../TestSceneDrawableRoomPlaylist.cs | 3 +-- .../TestSceneMultiplayerMatchSongSelect.cs | 2 +- .../TestScenePlaylistsSongSelect.cs | 3 +-- .../Navigation/TestScenePresentBeatmap.cs | 3 +-- .../Navigation/TestScenePresentScore.cs | 5 ++--- .../TestScenePlaylistsRoomCreation.cs | 7 +++--- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 2 +- .../TestSceneBeatmapRecommendations.cs | 3 +-- .../SongSelect/TestScenePlaySongSelect.cs | 13 +++++------ .../TestSceneDeleteLocalScore.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 5 ++--- osu.Game/Database/IModelImporter.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 2 +- osu.Game/Screens/Play/Player.cs | 8 ++++--- osu.Game/Skinning/SkinManager.cs | 5 ++--- osu.Game/Stores/RealmArchiveModelImporter.cs | 14 +++++++----- 21 files changed, 57 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index b37d2be0bc..69dd2d930a 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -685,7 +685,7 @@ namespace osu.Game.Tests.Database [Test] public void TestImportWithDuplicateBeatmapIDs() { - RunTestWithRealmAsync(async (realm, storage) => + RunTestWithRealm((realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); using var store = new RulesetStore(realm, storage); @@ -718,7 +718,7 @@ namespace osu.Game.Tests.Database } }; - var imported = await importer.Import(toImport); + var imported = importer.Import(toImport); Assert.NotNull(imported); Debug.Assert(imported != null); diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 144cbe15c3..95c15367aa 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Online addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing); AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); - AddUntilStep("wait for import", () => beatmaps.CurrentImportTask?.IsCompleted == true); + AddUntilStep("wait for import", () => beatmaps.CurrentImport != null); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); } @@ -164,7 +164,7 @@ namespace osu.Game.Tests.Online { public TaskCompletionSource AllowImport = new TaskCompletionSource(); - public Task> CurrentImportTask { get; private set; } + public ILive CurrentImport { get; private set; } public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap) @@ -186,10 +186,10 @@ namespace osu.Game.Tests.Online this.testBeatmapManager = testBeatmapManager; } - public override async Task> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public override ILive 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); + testBeatmapManager.AllowImport.Task.WaitSafely(); + return (testBeatmapManager.CurrentImport = base.Import(item, archive, lowPriority, cancellationToken)); } } } diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index dd12c94855..8de9f0a292 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; @@ -25,7 +24,7 @@ namespace osu.Game.Tests.Scores.IO public class ImportScoreTest : ImportTest { [Test] - public async Task TestBasicImport() + public void TestBasicImport() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { @@ -49,7 +48,7 @@ namespace osu.Game.Tests.Scores.IO BeatmapInfo = beatmap.Beatmaps.First() }; - var imported = await LoadScoreIntoOsu(osu, toImport); + var imported = LoadScoreIntoOsu(osu, toImport); Assert.AreEqual(toImport.Rank, imported.Rank); Assert.AreEqual(toImport.TotalScore, imported.TotalScore); @@ -67,7 +66,7 @@ namespace osu.Game.Tests.Scores.IO } [Test] - public async Task TestImportMods() + public void TestImportMods() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { @@ -85,7 +84,7 @@ namespace osu.Game.Tests.Scores.IO Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, }; - var imported = await LoadScoreIntoOsu(osu, toImport); + var imported = LoadScoreIntoOsu(osu, toImport); Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime)); @@ -98,7 +97,7 @@ namespace osu.Game.Tests.Scores.IO } [Test] - public async Task TestImportStatistics() + public void TestImportStatistics() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { @@ -120,7 +119,7 @@ namespace osu.Game.Tests.Scores.IO } }; - var imported = await LoadScoreIntoOsu(osu, toImport); + var imported = LoadScoreIntoOsu(osu, toImport); Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]); Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]); @@ -133,7 +132,7 @@ namespace osu.Game.Tests.Scores.IO } [Test] - public async Task TestOnlineScoreIsAvailableLocally() + public void TestOnlineScoreIsAvailableLocally() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { @@ -143,7 +142,7 @@ namespace osu.Game.Tests.Scores.IO var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); - await LoadScoreIntoOsu(osu, new ScoreInfo + LoadScoreIntoOsu(osu, new ScoreInfo { User = new APIUser { Username = "Test user" }, BeatmapInfo = beatmap.Beatmaps.First(), @@ -168,13 +167,14 @@ namespace osu.Game.Tests.Scores.IO } } - public static async Task LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) + public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) { // clone to avoid attaching the input score to realm. score = score.DeepClone(); var scoreManager = osu.Dependencies.Get(); - await scoreManager.Import(score, archive); + + scoreManager.Import(score, archive); return scoreManager.Query(_ => true); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 8199389b36..a12171401a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -147,7 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded); - AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true)).GetResultSafely()); + AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true))); AddUntilStep("state is available", () => downloadButton.State.Value == DownloadState.LocallyAvailable); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 3ebc64cd0b..10a82089b3 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Menus Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null; // ensure we have at least two beatmaps available to identify the direction the music controller navigated to. - AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()).WaitSafely(), 5); + AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5); AddStep("import beatmap with track", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 30ac1302aa..92accb0cd1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -8,7 +8,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; @@ -158,7 +157,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Debug.Assert(beatmap.BeatmapSet != null); - AddStep("import beatmap", () => imported = manager.Import(beatmap.BeatmapSet).GetResultSafely()); + AddStep("import beatmap", () => imported = manager.Import(beatmap.BeatmapSet)); createPlaylistWithBeatmaps(() => imported.PerformRead(s => s.Beatmaps.Detach())); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 7e3c9722cf..5465061891 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Multiplayer beatmapSetInfo.Beatmaps.Add(beatmap); } - manager.Import(beatmapSetInfo).WaitSafely(); + manager.Import(beatmapSetInfo); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index d7a0885c95..d933491ab6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -6,7 +6,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Utils; @@ -40,7 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var beatmapSet = TestResources.CreateTestBeatmapSetInfo(); - manager.Import(beatmapSet).WaitSafely(); + manager.Import(beatmapSet); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index f6c53e76c4..63226de750 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Extensions; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Extensions; @@ -125,7 +124,7 @@ namespace osu.Game.Tests.Visual.Navigation Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }, } - }).GetResultSafely()?.Value; + })?.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 7bd8110374..7656bf79dc 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Extensions; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -60,7 +59,7 @@ namespace osu.Game.Tests.Visual.Navigation Ruleset = new OsuRuleset().RulesetInfo }, } - }).GetResultSafely()?.Value; + })?.Value; }); } @@ -135,7 +134,7 @@ namespace osu.Game.Tests.Visual.Navigation BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo, User = new GuestUser(), - }).GetResultSafely().Value; + }).Value; }); AddAssert($"import {i} succeeded", () => imported != null); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index d397b37d05..68225f6d64 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -8,7 +8,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -151,7 +150,7 @@ namespace osu.Game.Tests.Visual.Playlists Debug.Assert(modifiedBeatmap.BeatmapInfo.BeatmapSet != null); - manager.Import(modifiedBeatmap.BeatmapInfo.BeatmapSet).WaitSafely(); + manager.Import(modifiedBeatmap.BeatmapInfo.BeatmapSet); }); // Create the room using the real beatmap values. @@ -196,7 +195,7 @@ namespace osu.Game.Tests.Visual.Playlists Debug.Assert(originalBeatmap.BeatmapInfo.BeatmapSet != null); - manager.Import(originalBeatmap.BeatmapInfo.BeatmapSet).WaitSafely(); + manager.Import(originalBeatmap.BeatmapInfo.BeatmapSet); }); AddUntilStep("match has correct beatmap", () => realHash == match.Beatmap.Value.BeatmapInfo.MD5Hash); @@ -219,7 +218,7 @@ namespace osu.Game.Tests.Visual.Playlists Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null); - importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet).GetResultSafely()?.Value.Detach(); + importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet)?.Value.Detach(); }); private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 5e977d075d..e31be1d51a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -180,7 +180,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep(@"Load new scores via manager", () => { foreach (var score in generateSampleScores(beatmapInfo())) - scoreManager.Import(score).WaitSafely(); + scoreManager.Import(score); }); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index b7bc0c37e1..940d001c5b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Extensions; @@ -184,7 +183,7 @@ namespace osu.Game.Tests.Visual.SongSelect beatmap.DifficultyName = $"SR{i + 1}"; } - return Game.BeatmapManager.Import(beatmapSet).GetResultSafely()?.Value; + return Game.BeatmapManager.Import(beatmapSet)?.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 0dc4556ed9..630171a4d0 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -8,7 +8,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -260,7 +259,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import multi-ruleset map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - manager.Import(TestResources.CreateTestBeatmapSetInfo(rulesets: usableRulesets)).WaitSafely(); + manager.Import(TestResources.CreateTestBeatmapSetInfo(rulesets: usableRulesets)); }); } else @@ -675,7 +674,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import multi-ruleset map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets)).WaitSafely(); + manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets)); }); int previousSetID = 0; @@ -715,7 +714,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import multi-ruleset map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets)).WaitSafely(); + manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets)); }); DrawableCarouselBeatmapSet set = null; @@ -764,7 +763,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import huge difficulty count map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - imported = manager.Import(TestResources.CreateTestBeatmapSetInfo(50, usableRulesets)).GetResultSafely()?.Value; + imported = manager.Import(TestResources.CreateTestBeatmapSetInfo(50, usableRulesets))?.Value; }); AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported.Beatmaps.First())); @@ -873,7 +872,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id)); - private void importForRuleset(int id) => manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == id).ToArray())).WaitSafely(); + private void importForRuleset(int id) => manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == id).ToArray())); private void checkMusicPlaying(bool playing) => AddUntilStep($"music {(playing ? "" : "not ")}playing", () => music.IsPlaying == playing); @@ -903,7 +902,7 @@ namespace osu.Game.Tests.Visual.SongSelect var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); for (int i = 0; i < 10; i++) - manager.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets)).WaitSafely(); + manager.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets)); }); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 55031a9699..4826d2fb33 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -112,7 +112,7 @@ namespace osu.Game.Tests.Visual.UserInterface Ruleset = new OsuRuleset().RulesetInfo, }; - importedScores.Add(scoreManager.Import(score).GetResultSafely().Value); + importedScores.Add(scoreManager.Import(score).Value); } }); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4488301fe5..a1c1982f00 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -10,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Audio; using osu.Framework.Audio.Track; -using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Framework.Testing; @@ -105,7 +104,7 @@ namespace osu.Game.Beatmaps foreach (BeatmapInfo b in beatmapSet.Beatmaps) b.BeatmapSet = beatmapSet; - var imported = beatmapModelManager.Import(beatmapSet).GetResultSafely(); + var imported = beatmapModelManager.Import(beatmapSet); if (imported == null) throw new InvalidOperationException("Failed to import new beatmap"); @@ -295,7 +294,7 @@ namespace osu.Game.Beatmaps return beatmapModelManager.Import(archive, lowPriority, cancellationToken); } - public Task?> Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public ILive? Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken); } diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index d00cfb2035..3047a1d30a 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -45,7 +45,7 @@ namespace osu.Game.Database /// 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); + ILive? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); /// /// A user displayable name for the model type associated with this manager. diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index bb26bd3b04..3a842a048a 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -308,7 +308,7 @@ namespace osu.Game.Scoring return scoreModelManager.Import(archive, lowPriority, cancellationToken); } - public Task> Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public ILive Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return scoreModelManager.Import(item, archive, lowPriority, cancellationToken); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4d3201cd27..cfca2d0a3d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1024,11 +1024,11 @@ namespace osu.Game.Screens.Play /// /// The to import. /// The imported score. - protected virtual async Task ImportScore(Score score) + protected virtual Task ImportScore(Score score) { // Replays are already populated and present in the game's database, so should not be re-imported. if (DrawableRuleset.ReplayScore != null) - return; + return Task.CompletedTask; LegacyByteArrayReader replayReader; @@ -1048,7 +1048,7 @@ namespace osu.Game.Screens.Play // conflicts across various systems (ie. solo and multiplayer). importableScore.OnlineID = -1; - var imported = await scoreManager.Import(importableScore, replayReader).ConfigureAwait(false); + var imported = scoreManager.Import(importableScore, replayReader); imported.PerformRead(s => { @@ -1056,6 +1056,8 @@ namespace osu.Game.Screens.Play score.ScoreInfo.Hash = s.Hash; score.ScoreInfo.ID = s.ID; }); + + return Task.CompletedTask; } /// diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 344acb7a0f..47c7bc060a 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -11,7 +11,6 @@ using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; @@ -151,7 +150,7 @@ namespace osu.Game.Skinning Name = s.Name + @" (modified)", Creator = s.Creator, InstantiationInfo = s.InstantiationInfo, - }).GetResultSafely(); + }); if (result != null) { @@ -278,7 +277,7 @@ namespace osu.Game.Skinning return skinModelManager.Import(archive, lowPriority, cancellationToken); } - public Task> Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public ILive Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return skinModelManager.Import(item, archive, lowPriority, cancellationToken); } diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index b0f676ecf0..43c1c7c888 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -250,8 +250,10 @@ namespace osu.Game.Stores return null; } - var scheduledImport = Task.Factory.StartNew(async () => await Import(model, archive, lowPriority, cancellationToken).ConfigureAwait(false), - cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap(); + var scheduledImport = Task.Factory.StartNew(() => Import(model, archive, lowPriority, cancellationToken), + cancellationToken, + TaskCreationOptions.HideScheduler, + lowPriority ? import_scheduler_low_priority : import_scheduler); return await scheduledImport.ConfigureAwait(false); } @@ -318,7 +320,7 @@ namespace osu.Game.Stores /// An optional archive to use for model population. /// Whether this is a low priority import. /// An optional cancellation token. - public virtual Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public virtual ILive? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return Realm.Run(realm => { @@ -353,7 +355,7 @@ namespace osu.Game.Stores transaction.Commit(); } - return Task.FromResult((ILive?)existing.ToLive(Realm)); + return existing.ToLive(Realm); } LogForModel(item, @"Found existing (optimised) but failed pre-check."); @@ -388,7 +390,7 @@ namespace osu.Game.Stores existing.DeletePending = false; transaction.Commit(); - return Task.FromResult((ILive?)existing.ToLive(Realm)); + return existing.ToLive(Realm); } LogForModel(item, @"Found existing but failed re-use check."); @@ -414,7 +416,7 @@ namespace osu.Game.Stores throw; } - return Task.FromResult((ILive?)item.ToLive(Realm)); + return (ILive?)item.ToLive(Realm); }); } From fc58b202b1d93b98d4d90b22741a5502591d5c3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 15:45:23 +0900 Subject: [PATCH 123/587] Fix crash when trying to migrate collection database that doesn't exist --- osu.Game/OsuGameBase.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7a6a126900..09ca4e450d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -208,8 +208,13 @@ namespace osu.Game realm.CreateBackup($"client.{migration}.realm"); using (var source = Storage.GetStream("collection.db")) - using (var destination = Storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew)) - source.CopyTo(destination); + { + if (source != null) + { + using (var destination = Storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew)) + source.CopyTo(destination); + } + } } dependencies.CacheAs(Storage); From 1bb1366c9f1c7e3d6d0b43f0bdf7d7f0ce3fe551 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 16:26:06 +0900 Subject: [PATCH 124/587] Fix notification reset events potentially arriving out of order if a block operation times out --- osu.Game/Database/RealmAccess.cs | 40 +++++++++++++++----------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e6e3c9ee41..bf88313663 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -334,25 +334,6 @@ namespace osu.Game.Database } } - /// - /// Unregister all subscriptions when the realm instance is to be recycled. - /// Subscriptions will still remain and will be re-subscribed when the realm instance returns. - /// - private void unregisterAllSubscriptions() - { - lock (realmLock) - { - foreach (var action in notificationsResetMap.Values) - action(); - - foreach (var action in customSubscriptionsResetMap) - { - action.Value?.Dispose(); - customSubscriptionsResetMap[action.Key] = null; - } - } - } - private Realm getRealmInstance() { if (isDisposed) @@ -605,9 +586,16 @@ namespace osu.Game.Database throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); syncContext = SynchronizationContext.Current; - } - unregisterAllSubscriptions(); + // Before disposing the update context, clean up all subscriptions. + // Note that in the case of realm notification subscriptions, this is not really required (they will be cleaned up by disposal). + // In the case of custom subscriptions, we want them to fire before the update realm is disposed in case they do any follow-up work. + foreach (var action in customSubscriptionsResetMap) + { + action.Value?.Dispose(); + customSubscriptionsResetMap[action.Key] = null; + } + } Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); @@ -615,6 +603,16 @@ namespace osu.Game.Database updateRealm = null; } + // In order to ensure events arrive in the correct order, these *must* be fired post disposal of the update realm, + // and must be posted to the synchronization context. + // This is because realm may fire event callbacks between the `unregisterAllSubscriptions` and `updateRealm.Dispose` + // calls above. + syncContext?.Post(_ => + { + foreach (var action in notificationsResetMap.Values) + action(); + }, null); + const int sleep_length = 200; int timeout = 5000; From 5a9524a74e945b6db0d51cdfa71574a63e6ee832 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 24 Jan 2022 02:51:13 +0300 Subject: [PATCH 125/587] Decrease default timeline zoom to "6 seconds visible" range --- osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 265f56534f..35a0282611 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -141,7 +141,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { MaxZoom = getZoomLevelForVisibleMilliseconds(500); MinZoom = getZoomLevelForVisibleMilliseconds(10000); - Zoom = getZoomLevelForVisibleMilliseconds(2000); + Zoom = getZoomLevelForVisibleMilliseconds(6000); } }, true); } From 1f9cf00db89f7a80027e665d86a3cfd8e1891b31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 16:44:44 +0900 Subject: [PATCH 126/587] Fix `DatabasedKeyBindingContainer` re-querying realm on receiving notification --- .../Bindings/DatabasedKeyBindingContainer.cs | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 3e4a9759a3..ba129b93e5 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Database; using osu.Game.Rulesets; +using Realms; namespace osu.Game.Input.Bindings { @@ -46,41 +47,36 @@ namespace osu.Game.Input.Bindings throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided."); } - private IQueryable queryRealmKeyBindings() - { - string rulesetName = ruleset?.ShortName; - return realm.Realm.All() - .Where(b => b.RulesetName == rulesetName && b.Variant == variant); - } - protected override void LoadComplete() { - realmSubscription = realm.RegisterForNotifications(r => queryRealmKeyBindings(), (sender, changes, error) => + realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, changes, error) => { // The first fire of this is a bit redundant as this is being called in base.LoadComplete, // but this is safest in case the subscription is restored after a context recycle. - ReloadMappings(); + reloadMappings(sender.AsQueryable()); }); base.LoadComplete(); } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); + protected override void ReloadMappings() => reloadMappings(queryRealmKeyBindings(realm.Realm)); - realmSubscription?.Dispose(); + private IQueryable queryRealmKeyBindings(Realm realm) + { + string rulesetName = ruleset?.ShortName; + return realm.All() + .Where(b => b.RulesetName == rulesetName && b.Variant == variant); } - protected override void ReloadMappings() + private void reloadMappings(IQueryable realmKeyBindings) { var defaults = DefaultKeyBindings.ToList(); - List newBindings = queryRealmKeyBindings().Detach() - // this ordering is important to ensure that we read entries from the database in the order - // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise - // have been eaten by the music controller due to query order. - .OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList(); + List newBindings = realmKeyBindings.Detach() + // this ordering is important to ensure that we read entries from the database in the order + // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise + // have been eaten by the music controller due to query order. + .OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList(); // In the case no bindings were found in the database, presume this usage is for a non-databased ruleset. // This actually should never be required and can be removed if it is ever deemed to cause a problem. @@ -91,5 +87,12 @@ namespace osu.Game.Input.Bindings else KeyBindings = newBindings; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + realmSubscription?.Dispose(); + } } } From 958cfde60834a0e5a9342c3dfc7f59893e52942d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 23:45:06 +0900 Subject: [PATCH 127/587] Stop detaching and exposing beatmaps from `MusicController` --- osu.Game/Overlays/MusicController.cs | 14 ++++++-------- osu.Game/Overlays/NowPlayingOverlay.cs | 1 - 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 9dc55d24dd..335abb53dc 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -30,8 +30,6 @@ namespace osu.Game.Overlays [Resolved] private BeatmapManager beatmaps { get; set; } - public IBindableList BeatmapSets => beatmapSets; - /// /// Point in time after which the current track will be restarted on triggering a "previous track" action. /// @@ -88,12 +86,12 @@ namespace osu.Game.Overlays { beatmapSets.Clear(); foreach (var s in sender) - beatmapSets.Add(s.Detach()); + beatmapSets.Add(s); return; } foreach (int i in changes.InsertedIndices) - beatmapSets.Insert(i, sender[i].Detach()); + beatmapSets.Insert(i, sender[i]); foreach (int i in changes.DeletedIndices.OrderByDescending(i => i)) beatmapSets.RemoveAt(i); @@ -240,7 +238,7 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Prev; - var playable = BeatmapSets.TakeWhile(i => !i.Equals(current.BeatmapSetInfo)).LastOrDefault() ?? BeatmapSets.LastOrDefault(); + var playable = beatmapSets.TakeWhile(i => !i.Equals(current.BeatmapSetInfo)).LastOrDefault() ?? beatmapSets.LastOrDefault(); if (playable != null) { @@ -271,7 +269,7 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Next; - var playableSet = BeatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).ElementAtOrDefault(1) ?? BeatmapSets.FirstOrDefault(); + var playableSet = beatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).ElementAtOrDefault(1) ?? beatmapSets.FirstOrDefault(); var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault(); if (playableBeatmap != null) @@ -322,8 +320,8 @@ namespace osu.Game.Overlays else { // figure out the best direction based on order in playlist. - int last = BeatmapSets.TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count(); - int next = newWorking == null ? -1 : BeatmapSets.TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count(); + int last = beatmapSets.TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count(); + int next = newWorking == null ? -1 : beatmapSets.TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count(); direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 4dd23c0008..4617a91885 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -197,7 +197,6 @@ namespace osu.Game.Overlays { dragContainer.Add(playlist); - playlist.BeatmapSets.BindTo(musicController.BeatmapSets); playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true); togglePlaylist(); From 8a4f3a7ce093b02dbbfa655146a30308214330d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jan 2022 01:18:21 +0900 Subject: [PATCH 128/587] Reimplement subscription logic in `PlaylistOverlay` directly --- osu.Game/Overlays/Music/PlaylistOverlay.cs | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 78b2d58dae..245886743a 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -10,9 +11,11 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics; using osuTK; using osuTK.Graphics; +using Realms; namespace osu.Game.Overlays.Music { @@ -30,6 +33,15 @@ namespace osu.Game.Overlays.Music [Resolved] private BeatmapManager beatmaps { get; set; } + [Resolved] + private RealmContextFactory realmFactory { get; set; } + + private IDisposable beatmapSubscription; + + private IQueryable availableBeatmaps => realmFactory.Context + .All() + .Where(s => !s.DeletePending); + private FilterControl filter; private Playlist list; @@ -91,10 +103,31 @@ namespace osu.Game.Overlays.Music { base.LoadComplete(); + // tests might bind externally, in which case we don't want to involve realm. + if (beatmapSets.Count == 0) + beatmapSubscription = realmFactory.RegisterForNotifications(realm => availableBeatmaps, beatmapsChanged); + list.Items.BindTo(beatmapSets); beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo, true); } + private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + { + if (changes == null) + { + beatmapSets.Clear(); + // must use AddRange to avoid RearrangeableList sort overhead per add op. + beatmapSets.AddRange(sender); + return; + } + + foreach (int i in changes.InsertedIndices) + beatmapSets.Insert(i, sender[i]); + + foreach (int i in changes.DeletedIndices.OrderByDescending(i => i)) + beatmapSets.RemoveAt(i); + } + protected override void PopIn() { filter.Search.HoldFocus = true; @@ -123,5 +156,11 @@ namespace osu.Game.Overlays.Music beatmap.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First()); beatmap.Value.Track.Restart(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + beatmapSubscription?.Dispose(); + } } } From ace2bd2208eead324a2ad8753b4c33bb6e1d02ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jan 2022 01:18:29 +0900 Subject: [PATCH 129/587] Apply some initial optimisations to `PlaylistItem` --- osu.Game/Overlays/Music/PlaylistItem.cs | 44 +++++++++++-------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 04c12b8cd7..b3b039148b 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -32,8 +32,6 @@ namespace osu.Game.Overlays.Music : base(item) { Padding = new MarginPadding { Left = 5 }; - - FilterTerms = item.Metadata.GetSearchableTerms(); } [BackgroundDependencyLoader] @@ -46,6 +44,25 @@ namespace osu.Game.Overlays.Music { base.LoadComplete(); + var metadata = Model.Metadata; + + var title = new RomanisableString(metadata.TitleUnicode, metadata.Title); + var artist = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); + + titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)); + + updateSelectionState(true); + titlePart.DrawablePartsRecreated += _ => updateSelectionState(true); + + text.AddText(@" "); // to separate the title from the artist. + + text.AddText(artist, sprite => + { + sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); + sprite.Colour = colours.Gray9; + sprite.Padding = new MarginPadding { Top = 1 }; + }); + SelectedSet.BindValueChanged(set => { if (set.OldValue?.Equals(Model) != true && set.NewValue?.Equals(Model) != true) @@ -67,27 +84,6 @@ namespace osu.Game.Overlays.Music AutoSizeAxes = Axes.Y, }; - protected override void LoadAsyncComplete() - { - base.LoadAsyncComplete(); - - var title = new RomanisableString(Model.Metadata.TitleUnicode, Model.Metadata.Title); - var artist = new RomanisableString(Model.Metadata.ArtistUnicode, Model.Metadata.Artist); - - titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)); - updateSelectionState(true); - titlePart.DrawablePartsRecreated += _ => updateSelectionState(true); - - text.AddText(@" "); // to separate the title from the artist. - - text.AddText(artist, sprite => - { - sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); - sprite.Colour = colours.Gray9; - sprite.Padding = new MarginPadding { Top = 1 }; - }); - } - protected override bool OnClick(ClickEvent e) { RequestSelection?.Invoke(Model); @@ -109,7 +105,7 @@ namespace osu.Game.Overlays.Music } } - public IEnumerable FilterTerms { get; } + public IEnumerable FilterTerms => Model.Metadata.GetSearchableTerms(); private bool matchingFilter = true; From 2a786f9ec0302f3ba39561ae5a5d56a0eafa7fe5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jan 2022 01:49:07 +0900 Subject: [PATCH 130/587] Load text only after it comes on screen (and tidy up selection handling logic) --- osu.Game/Overlays/Music/PlaylistItem.cs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index b3b039148b..b31a03a64d 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -50,12 +50,9 @@ namespace osu.Game.Overlays.Music var artist = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)); - - updateSelectionState(true); titlePart.DrawablePartsRecreated += _ => updateSelectionState(true); text.AddText(@" "); // to separate the title from the artist. - text.AddText(artist, sprite => { sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); @@ -65,24 +62,31 @@ namespace osu.Game.Overlays.Music SelectedSet.BindValueChanged(set => { - if (set.OldValue?.Equals(Model) != true && set.NewValue?.Equals(Model) != true) + bool newSelected = set.NewValue?.Equals(Model) == true; + + if (newSelected == selected) return; + selected = newSelected; updateSelectionState(false); - }, true); + }); + + updateSelectionState(true); } + private bool selected; + private void updateSelectionState(bool instant) { foreach (Drawable s in titlePart.Drawables) - s.FadeColour(SelectedSet.Value?.Equals(Model) == true ? colours.Yellow : Color4.White, instant ? 0 : FADE_DURATION); + s.FadeColour(selected ? colours.Yellow : Color4.White, instant ? 0 : FADE_DURATION); } - protected override Drawable CreateContent() => text = new OsuTextFlowContainer + protected override Drawable CreateContent() => new DelayedLoadWrapper(text = new OsuTextFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - }; + }); protected override bool OnClick(ClickEvent e) { From 83b0e4572a057c520723c3319d7227c1ccf8951f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jan 2022 02:39:01 +0900 Subject: [PATCH 131/587] Fix test failures --- osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index 62f3b63780..7131c19471 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("drag to 5th", () => { var item = this.ChildrenOfType().ElementAt(4); - InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.Centre); + InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.BottomLeft); }); AddAssert("song 1 is 5th", () => beatmapSets[4].Equals(first)); From 1a776a95877907b7d9304ffa6ac4a529558f1b79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jan 2022 04:27:07 +0900 Subject: [PATCH 132/587] Completely remove subscription from `MusicController` --- osu.Game/Overlays/Music/PlaylistOverlay.cs | 10 ++-- osu.Game/Overlays/MusicController.cs | 68 ++++------------------ 2 files changed, 16 insertions(+), 62 deletions(-) diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 245886743a..6adcd2a6d6 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -34,13 +34,13 @@ namespace osu.Game.Overlays.Music private BeatmapManager beatmaps { get; set; } [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } private IDisposable beatmapSubscription; - private IQueryable availableBeatmaps => realmFactory.Context - .All() - .Where(s => !s.DeletePending); + private IQueryable availableBeatmaps => realm.Realm + .All() + .Where(s => !s.DeletePending); private FilterControl filter; private Playlist list; @@ -105,7 +105,7 @@ namespace osu.Game.Overlays.Music // tests might bind externally, in which case we don't want to involve realm. if (beatmapSets.Count == 0) - beatmapSubscription = realmFactory.RegisterForNotifications(realm => availableBeatmaps, beatmapsChanged); + beatmapSubscription = realm.RegisterForNotifications(realm => availableBeatmaps, beatmapsChanged); list.Items.BindTo(beatmapSets); beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo, true); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 335abb53dc..5fc0da8891 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -16,7 +16,6 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Rulesets.Mods; -using Realms; namespace osu.Game.Overlays { @@ -25,8 +24,6 @@ namespace osu.Game.Overlays /// public class MusicController : CompositeDrawable { - private IDisposable beatmapSubscription; - [Resolved] private BeatmapManager beatmaps { get; set; } @@ -35,8 +32,6 @@ namespace osu.Game.Overlays /// private const double restart_cutoff_point = 5000; - private readonly BindableList beatmapSets = new BindableList(); - /// /// Whether the user has requested the track to be paused. Use to determine whether the track is still playing. /// @@ -69,50 +64,11 @@ namespace osu.Game.Overlays mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } - private IQueryable queryRealmBeatmapSets() => - realm.Realm - .All() - .Where(s => !s.DeletePending); - - protected override void LoadComplete() - { - base.LoadComplete(); - beatmapSubscription = realm.RegisterForNotifications(r => queryRealmBeatmapSets(), beatmapsChanged); - } - - private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) - { - if (changes == null) - { - beatmapSets.Clear(); - foreach (var s in sender) - beatmapSets.Add(s); - return; - } - - foreach (int i in changes.InsertedIndices) - beatmapSets.Insert(i, sender[i]); - - foreach (int i in changes.DeletedIndices.OrderByDescending(i => i)) - beatmapSets.RemoveAt(i); - } - /// /// Forcefully reload the current 's track from disk. /// public void ReloadCurrentTrack() => changeTrack(); - /// - /// Change the position of a in the current playlist. - /// - /// The beatmap to move. - /// The new position. - public void ChangeBeatmapSetPosition(BeatmapSetInfo beatmapSetInfo, int index) - { - beatmapSets.Remove(beatmapSetInfo); - beatmapSets.Insert(index, beatmapSetInfo); - } - /// /// Returns whether the beatmap track is playing. /// @@ -238,11 +194,12 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Prev; - var playable = beatmapSets.TakeWhile(i => !i.Equals(current.BeatmapSetInfo)).LastOrDefault() ?? beatmapSets.LastOrDefault(); + var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current.BeatmapSetInfo)).LastOrDefault() + ?? getBeatmapSets().LastOrDefault(); - if (playable != null) + if (playableSet != null) { - changeBeatmap(beatmaps.GetWorkingBeatmap(playable.Beatmaps.First())); + changeBeatmap(beatmaps.GetWorkingBeatmap(playableSet.Beatmaps.First())); restartTrack(); return PreviousTrackResult.Previous; } @@ -269,7 +226,9 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Next; - var playableSet = beatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).ElementAtOrDefault(1) ?? beatmapSets.FirstOrDefault(); + var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current.BeatmapSetInfo)).ElementAtOrDefault(1) + ?? getBeatmapSets().FirstOrDefault(); + var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault(); if (playableBeatmap != null) @@ -293,6 +252,8 @@ namespace osu.Game.Overlays private TrackChangeDirection? queuedDirection; + private IQueryable getBeatmapSets() => realm.Realm.All().Where(s => !s.DeletePending); + private void beatmapChanged(ValueChangedEvent beatmap) => changeBeatmap(beatmap.NewValue); private void changeBeatmap(WorkingBeatmap newWorking) @@ -320,8 +281,8 @@ namespace osu.Game.Overlays else { // figure out the best direction based on order in playlist. - int last = beatmapSets.TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count(); - int next = newWorking == null ? -1 : beatmapSets.TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count(); + int last = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count(); + int next = newWorking == null ? -1 : getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count(); direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; } @@ -435,13 +396,6 @@ namespace osu.Game.Overlays mod.ApplyToTrack(CurrentTrack); } } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - beatmapSubscription?.Dispose(); - } } public enum TrackChangeDirection From ffd7877a1e8b6b822fd5f5922111b3d1ae7c027b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 17:41:22 +0900 Subject: [PATCH 133/587] Remove synchronization context hacks in realm tests --- osu.Game.Tests/Database/GeneralUsageTests.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index dc0d42595b..8262ef18d4 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -87,11 +87,6 @@ namespace osu.Game.Tests.Database hasThreadedUsage.Wait(); - // Usually the host would run the synchronization context work per frame. - // For the sake of keeping this test simple (there's only one update invocation), - // let's replace it so we can ensure work is run immediately. - SynchronizationContext.SetSynchronizationContext(new ImmediateExecuteSynchronizationContext()); - Assert.Throws(() => { using (realm.BlockAllOperations()) @@ -107,10 +102,5 @@ namespace osu.Game.Tests.Database } }); } - - private class ImmediateExecuteSynchronizationContext : SynchronizationContext - { - public override void Post(SendOrPostCallback d, object? state) => d(state); - } } } From 90a7dd771107f0de718a1fefc286c2d946ee99c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 14:30:00 +0900 Subject: [PATCH 134/587] In gameplay bindings test, ensure a selection is made before attempting to enter gameplay --- .../Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index bfcefdbbfe..6165c9d9b1 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -50,8 +50,11 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("close settings", () => Game.Settings.Hide()); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + PushAndConfirm(() => new PlaySongSelect()); + AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault); + AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false); From c0ed30801686e2a25486fb2a80a9ec2534e2b522 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 14:30:32 +0900 Subject: [PATCH 135/587] Use more correct method of deletion in `TestScenePlaySongSelect` --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 630171a4d0..458c6130c7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -64,13 +64,13 @@ namespace osu.Game.Tests.Visual.SongSelect { base.SetUpSteps(); - AddStep("delete all beatmaps", () => + AddStep("reset defaults", () => { Ruleset.Value = new OsuRuleset().RulesetInfo; - manager?.Delete(manager.GetAllUsableBeatmapSets()); - Beatmap.SetDefault(); }); + + AddStep("delete all beatmaps", () => manager?.Delete()); } [Test] From b2b6672095d10a8da9a005dd87d072212893e454 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jan 2022 11:43:05 +0300 Subject: [PATCH 136/587] Add failing test asserts --- osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index bf3b46c6f7..96f815621c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; @@ -40,6 +41,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddStep("Set beat divisor", () => editor.Dependencies.Get().Value = 16); AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7); AddStep("Set artist and title", () => { @@ -88,6 +90,7 @@ namespace osu.Game.Tests.Visual.Editing private void checkMutations() { AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); + AddAssert("Beatmap has correct beat divisor", () => editorBeatmap.BeatmapInfo.BeatDivisor == 16); AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7); AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title"); AddAssert("Beatmap has correct author", () => editorBeatmap.BeatmapInfo.Metadata.Author.Username == "author"); From f7f58b06a1bd107100d3ed0ee23c65bb0925112c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jan 2022 11:43:16 +0300 Subject: [PATCH 137/587] Fix beat divisor not saving in editor --- osu.Game/Screens/Edit/Editor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8c4b458534..b42f629aad 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -158,9 +158,6 @@ namespace osu.Game.Screens.Edit return; } - beatDivisor.Value = playableBeatmap.BeatmapInfo.BeatDivisor; - beatDivisor.BindValueChanged(divisor => playableBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); - // Todo: should probably be done at a DrawableRuleset level to share logic with Player. clock = new EditorClock(playableBeatmap, beatDivisor) { IsCoupled = false }; clock.ChangeSource(loadableBeatmap.Track); @@ -178,6 +175,9 @@ namespace osu.Game.Screens.Edit changeHandler = new EditorChangeHandler(editorBeatmap); dependencies.CacheAs(changeHandler); + beatDivisor.Value = editorBeatmap.BeatmapInfo.BeatDivisor; + beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); + updateLastSavedHash(); Schedule(() => From a93873e8ca0f9290c5be236b363337d71042824f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jan 2022 13:01:30 +0300 Subject: [PATCH 138/587] Recreate test beatmap of `EditorTestScene` on set up --- osu.Game/Tests/Visual/EditorTestScene.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 6f751b1736..f7d62a8694 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -43,28 +43,16 @@ namespace osu.Game.Tests.Visual }; private TestBeatmapManager testBeatmapManager; - private WorkingBeatmap working; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio, RulesetStore rulesets) { Add(logo); - working = CreateWorkingBeatmap(Ruleset.Value); - if (IsolateSavingFromDatabase) Dependencies.CacheAs(testBeatmapManager = new TestBeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); } - protected override void LoadComplete() - { - base.LoadComplete(); - - Beatmap.Value = working; - if (testBeatmapManager != null) - testBeatmapManager.TestBeatmap = working; - } - protected virtual bool EditorComponentsReady => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true; @@ -78,6 +66,11 @@ namespace osu.Game.Tests.Visual protected virtual void LoadEditor() { + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); + + if (testBeatmapManager != null) + testBeatmapManager.TestBeatmap = Beatmap.Value; + LoadScreen(editorLoader = new TestEditorLoader()); } From 4a9f4eecba45e66428de23094f1d1387199bb450 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 20:49:52 +0900 Subject: [PATCH 139/587] Use blocking calls to `SynchronizationContext` to guarantee order of execution --- osu.Game/Database/RealmAccess.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index bf88313663..ffaae9b4ae 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -607,7 +607,7 @@ namespace osu.Game.Database // and must be posted to the synchronization context. // This is because realm may fire event callbacks between the `unregisterAllSubscriptions` and `updateRealm.Dispose` // calls above. - syncContext?.Post(_ => + syncContext?.Send(_ => { foreach (var action in notificationsResetMap.Values) action(); @@ -647,8 +647,14 @@ namespace osu.Game.Database { Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); realmRetrievalLock.Release(); + // Post back to the update thread to revive any subscriptions. - syncContext?.Post(_ => ensureUpdateRealm(), null); + // In the case we are on the update thread, let's also require this to run synchronously. + // This requirement is mostly due to test coverage, but shouldn't cause any harm. + if (ThreadSafety.IsUpdateThread) + syncContext?.Send(_ => ensureUpdateRealm(), null); + else + syncContext?.Post(_ => ensureUpdateRealm(), null); } } From 6c69df815a83f718d59354744321ce99ca5b644a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jan 2022 15:25:28 +0300 Subject: [PATCH 140/587] Update editor test scenes to set working beatmap properly --- .../Visual/Editing/TestSceneDifficultySwitching.cs | 8 +++----- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 7 ++----- .../Visual/Editing/TestSceneEditorTestGameplay.cs | 5 ++++- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs index 243bb71e26..cf6488f721 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; +using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps.IO; namespace osu.Game.Tests.Visual.Editing @@ -37,11 +38,8 @@ namespace osu.Game.Tests.Visual.Editing base.SetUpSteps(); } - protected override void LoadEditor() - { - Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First()); - base.LoadEditor(); - } + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + => beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First()); [Test] public void TestBasicSwitch() diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 2386446e96..e3fb44534b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; +using osu.Game.Storyboards; using osu.Game.Tests.Resources; using SharpCompress.Archives; using SharpCompress.Archives.Zip; @@ -39,11 +40,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString()); } - protected override void LoadEditor() - { - Beatmap.Value = new DummyWorkingBeatmap(Audio, null); - base.LoadEditor(); - } + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new DummyWorkingBeatmap(Audio, null); [Test] public void TestCreateNewBeatmap() diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index bb630e5d5c..79afc8cf27 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -17,6 +17,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Play; +using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps.IO; using osuTK.Graphics; using osuTK.Input; @@ -43,9 +44,11 @@ namespace osu.Game.Tests.Visual.Editing base.SetUpSteps(); } + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + => beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.RulesetID == 0)); + protected override void LoadEditor() { - Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.RulesetID == 0)); SelectedMods.Value = new[] { new ModCinema() }; base.LoadEditor(); } From cdef67ccd0840053192a151b9d75f303e44ecb07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jan 2022 23:38:46 +0900 Subject: [PATCH 141/587] Log posted notifications To help with test failures and the likes. --- osu.Game/Overlays/NotificationOverlay.cs | 3 +++ osu.Game/Overlays/Notifications/Notification.cs | 3 +++ osu.Game/Overlays/Notifications/ProgressNotification.cs | 2 +- osu.Game/Overlays/Notifications/SimpleNotification.cs | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 8809dec642..e4e3931048 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Localisation; @@ -118,6 +119,8 @@ namespace osu.Game.Overlays { ++runningDepth; + Logger.Log($"⚠️ {notification.Text}"); + notification.Closed += notificationClosed; if (notification is IHasCompletionTarget hasCompletionTarget) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 44203e8ee7..ec6e9e09b3 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; @@ -25,6 +26,8 @@ namespace osu.Game.Overlays.Notifications /// public event Action Closed; + public abstract LocalisableString Text { get; set; } + /// /// Whether this notification should forcefully display itself. /// diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 5b74bff817..4735fcb7c1 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Notifications private LocalisableString text; - public LocalisableString Text + public override LocalisableString Text { get => text; set diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs index c32e40ffc8..b9a1cc6d90 100644 --- a/osu.Game/Overlays/Notifications/SimpleNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Notifications { private LocalisableString text; - public LocalisableString Text + public override LocalisableString Text { get => text; set From d1cbdf63f09afb381961cc646782d2c01610f80d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jan 2022 10:43:43 +0300 Subject: [PATCH 142/587] Add support for reading/saving timeline zoom in editor --- .../Compose/Components/Timeline/Timeline.cs | 29 ++++++++++++++++--- .../Timeline/ZoomableScrollContainer.cs | 9 ++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 35a0282611..5722174490 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -24,7 +24,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Cached] public class Timeline : ZoomableScrollContainer, IPositionSnapProvider { + private const float timeline_height = 72; + private const float timeline_expanded_height = 94; + private readonly Drawable userContent; + public readonly Bindable WaveformVisible = new Bindable(); public readonly Bindable ControlPointsVisible = new Bindable(); @@ -58,8 +62,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Track track; - private const float timeline_height = 72; - private const float timeline_expanded_height = 94; + /// + /// The timeline zoom level at a 1x zoom scale. + /// + private float defaultTimelineZoom; + + private readonly Bindable timelineZoomScale = new BindableDouble(1.0); public Timeline(Drawable userContent) { @@ -84,7 +92,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Bindable waveformOpacity; [BackgroundDependencyLoader] - private void load(IBindable beatmap, OsuColour colours, OsuConfigManager config) + private void load(IBindable beatmap, EditorBeatmap editorBeatmap, OsuColour colours, OsuConfigManager config) { CentreMarker centreMarker; @@ -141,9 +149,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { MaxZoom = getZoomLevelForVisibleMilliseconds(500); MinZoom = getZoomLevelForVisibleMilliseconds(10000); - Zoom = getZoomLevelForVisibleMilliseconds(6000); + defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000); } }, true); + + timelineZoomScale.Value = editorBeatmap.BeatmapInfo.TimelineZoom; + timelineZoomScale.BindValueChanged(scale => + { + Zoom = (float)(defaultTimelineZoom * scale.NewValue); + editorBeatmap.BeatmapInfo.TimelineZoom = scale.NewValue; + }, true); } protected override void LoadComplete() @@ -201,6 +216,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return base.OnScroll(e); } + protected override void OnZoomChange() + { + base.OnZoomChange(); + timelineZoomScale.Value = Zoom / defaultTimelineZoom; + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index f10eb0d284..6dea99f1c8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -136,11 +136,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { zoomTarget = Math.Clamp(newZoom, MinZoom, MaxZoom); transformZoomTo(zoomTarget, focusPoint, ZoomDuration, ZoomEasing); + + OnZoomChange(); } private void transformZoomTo(float newZoom, float focusPoint, double duration = 0, Easing easing = Easing.None) => this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, Current), newZoom, duration, easing)); + /// + /// Invoked when the zoom target has changed. + /// + protected virtual void OnZoomChange() + { + } + private class TransformZoom : Transform { /// From ad18bc4983254b389c5b80f707ee8610197a5ceb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jan 2022 17:02:23 +0300 Subject: [PATCH 143/587] Update timeline selection test scene with zoom changes --- .../Editing/TestSceneTimelineSelection.cs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 2544b6c2a1..81ab4712ab 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -47,25 +47,25 @@ namespace osu.Game.Tests.Visual.Editing AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { - new HitCircle { StartTime = 100 }, - new HitCircle { StartTime = 200, Position = new Vector2(100) }, - new HitCircle { StartTime = 300, Position = new Vector2(200) }, - new HitCircle { StartTime = 400, Position = new Vector2(300) }, + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 1000, Position = new Vector2(100) }, + new HitCircle { StartTime = 1500, Position = new Vector2(200) }, + new HitCircle { StartTime = 2000, Position = new Vector2(300) }, })); AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); AddStep("nudge forwards", () => InputManager.Key(Key.K)); - AddAssert("objects moved forwards in time", () => addedObjects[0].StartTime > 100); + AddAssert("objects moved forwards in time", () => addedObjects[0].StartTime > 500); AddStep("nudge backwards", () => InputManager.Key(Key.J)); - AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100); + AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 500); } [Test] public void TestBasicSelect() { - var addedObject = new HitCircle { StartTime = 100 }; + var addedObject = new HitCircle { StartTime = 500 }; AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); moveMouseToObject(() => addedObject); @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Editing var addedObject2 = new HitCircle { - StartTime = 200, + StartTime = 1000, Position = new Vector2(100), }; @@ -92,10 +92,10 @@ namespace osu.Game.Tests.Visual.Editing { var addedObjects = new[] { - new HitCircle { StartTime = 100 }, - new HitCircle { StartTime = 200, Position = new Vector2(100) }, - new HitCircle { StartTime = 300, Position = new Vector2(200) }, - new HitCircle { StartTime = 400, Position = new Vector2(300) }, + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 1000, Position = new Vector2(100) }, + new HitCircle { StartTime = 1500, Position = new Vector2(200) }, + new HitCircle { StartTime = 2000, Position = new Vector2(300) }, }; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); @@ -125,7 +125,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestBasicDeselect() { - var addedObject = new HitCircle { StartTime = 100 }; + var addedObject = new HitCircle { StartTime = 500 }; AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); moveMouseToObject(() => addedObject); @@ -166,11 +166,11 @@ namespace osu.Game.Tests.Visual.Editing { var addedObjects = new[] { - new HitCircle { StartTime = 100 }, - new HitCircle { StartTime = 200, Position = new Vector2(100) }, - new HitCircle { StartTime = 300, Position = new Vector2(200) }, - new HitCircle { StartTime = 400, Position = new Vector2(300) }, - new HitCircle { StartTime = 500, Position = new Vector2(400) }, + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 1000, Position = new Vector2(100) }, + new HitCircle { StartTime = 1500, Position = new Vector2(200) }, + new HitCircle { StartTime = 2000, Position = new Vector2(300) }, + new HitCircle { StartTime = 2500, Position = new Vector2(400) }, }; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); @@ -236,10 +236,10 @@ namespace osu.Game.Tests.Visual.Editing { var addedObjects = new[] { - new HitCircle { StartTime = 100 }, - new HitCircle { StartTime = 200, Position = new Vector2(100) }, - new HitCircle { StartTime = 300, Position = new Vector2(200) }, - new HitCircle { StartTime = 400, Position = new Vector2(300) }, + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 1000, Position = new Vector2(100) }, + new HitCircle { StartTime = 1500, Position = new Vector2(200) }, + new HitCircle { StartTime = 2000, Position = new Vector2(300) }, }; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); From 4169e5592ee973349fc5f39e1778b948ac722338 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jan 2022 19:36:19 +0300 Subject: [PATCH 144/587] Reword event handler name and update xmldoc --- .../Screens/Edit/Compose/Components/Timeline/Timeline.cs | 4 ++-- .../Compose/Components/Timeline/ZoomableScrollContainer.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 5722174490..51cca4ceff 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -216,9 +216,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return base.OnScroll(e); } - protected override void OnZoomChange() + protected override void OnZoomChanged() { - base.OnZoomChange(); + base.OnZoomChanged(); timelineZoomScale.Value = Zoom / defaultTimelineZoom; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 6dea99f1c8..35d103ddf1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -137,16 +137,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline zoomTarget = Math.Clamp(newZoom, MinZoom, MaxZoom); transformZoomTo(zoomTarget, focusPoint, ZoomDuration, ZoomEasing); - OnZoomChange(); + OnZoomChanged(); } private void transformZoomTo(float newZoom, float focusPoint, double duration = 0, Easing easing = Easing.None) => this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, Current), newZoom, duration, easing)); /// - /// Invoked when the zoom target has changed. + /// Invoked when has changed. /// - protected virtual void OnZoomChange() + protected virtual void OnZoomChanged() { } From 5085eb68018d1ae069cce9e754d5e6af326d3a06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 03:37:11 +0900 Subject: [PATCH 145/587] Ensure gameplay starts by dismissing any notifications in `TestSceneChangeAndUseGameplayBindings` --- .../TestSceneChangeAndUseGameplayBindings.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index 6165c9d9b1..347b4b6c54 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -57,6 +57,13 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); + AddUntilStep("wait for player", () => + { + // dismiss any notifications that may appear (ie. muted notification). + clickMouseInCentre(); + return player != null; + }); + AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false); AddStep("press 'z'", () => InputManager.Key(Key.Z)); @@ -66,6 +73,12 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("key counter did increase", () => keyCounter.CountPresses == 1); } + private void clickMouseInCentre() + { + InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Left); + } + private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel .ChildrenOfType() .FirstOrDefault(s => s.Ruleset.ShortName == "osu"); From 64914c45a4d7528713b8d06d3e7d54c2335db2cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 11:53:50 +0900 Subject: [PATCH 146/587] Remove unnecessary realm query helper method --- osu.Game/Overlays/Music/PlaylistOverlay.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 6adcd2a6d6..ffddf168ed 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -38,10 +38,6 @@ namespace osu.Game.Overlays.Music private IDisposable beatmapSubscription; - private IQueryable availableBeatmaps => realm.Realm - .All() - .Where(s => !s.DeletePending); - private FilterControl filter; private Playlist list; @@ -105,7 +101,7 @@ namespace osu.Game.Overlays.Music // tests might bind externally, in which case we don't want to involve realm. if (beatmapSets.Count == 0) - beatmapSubscription = realm.RegisterForNotifications(realm => availableBeatmaps, beatmapsChanged); + beatmapSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending), beatmapsChanged); list.Items.BindTo(beatmapSets); beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo, true); From dda513dd08f4aad11b847fef0da0ef4e669f35b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 12:19:05 +0900 Subject: [PATCH 147/587] Change `PlaylistOverlay` to use `ILive` --- .../UserInterface/TestScenePlaylistOverlay.cs | 9 +-- osu.Game/Collections/BeatmapCollection.cs | 2 +- .../Collections/CollectionFilterDropdown.cs | 4 +- osu.Game/Overlays/Music/Playlist.cs | 17 ++--- osu.Game/Overlays/Music/PlaylistItem.cs | 66 ++++++++++--------- osu.Game/Overlays/Music/PlaylistOverlay.cs | 42 +++++++----- 6 files changed, 76 insertions(+), 64 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index 7131c19471..1a3e38ddd7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Music; using osu.Game.Tests.Resources; @@ -18,11 +19,11 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene { - private readonly BindableList beatmapSets = new BindableList(); + private readonly BindableList> beatmapSets = new BindableList>(); private PlaylistOverlay playlistOverlay; - private BeatmapSetInfo first; + private ILive first; [SetUp] public void Setup() => Schedule(() => @@ -45,7 +46,7 @@ namespace osu.Game.Tests.Visual.UserInterface for (int i = 0; i < 100; i++) { - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo()); + beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo().ToLiveUnmanaged()); } first = beatmapSets.First(); @@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("hold 1st item handle", () => { - var handle = this.ChildrenOfType.PlaylistItemHandle>().First(); + var handle = this.ChildrenOfType>.PlaylistItemHandle>().First(); InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre); InputManager.PressButton(MouseButton.Left); }); diff --git a/osu.Game/Collections/BeatmapCollection.cs b/osu.Game/Collections/BeatmapCollection.cs index 1a739f824f..7e4b15ecf9 100644 --- a/osu.Game/Collections/BeatmapCollection.cs +++ b/osu.Game/Collections/BeatmapCollection.cs @@ -25,7 +25,7 @@ namespace osu.Game.Collections /// /// The beatmaps contained by the collection. /// - public readonly BindableList Beatmaps = new BindableList(); + public readonly BindableList Beatmaps = new BindableList(); /// /// The date when this collection was last modified. diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index 77bda00107..c46ba8e06e 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -38,7 +38,7 @@ namespace osu.Game.Collections } private readonly IBindableList collections = new BindableList(); - private readonly IBindableList beatmaps = new BindableList(); + private readonly IBindableList beatmaps = new BindableList(); private readonly BindableList filters = new BindableList(); [Resolved(CanBeNull = true)] @@ -196,7 +196,7 @@ namespace osu.Game.Collections private IBindable beatmap { get; set; } [CanBeNull] - private readonly BindableList collectionBeatmaps; + private readonly BindableList collectionBeatmaps; [NotNull] private readonly Bindable collectionName; diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs index 0b15a3a1bc..c86146ff25 100644 --- a/osu.Game/Overlays/Music/Playlist.cs +++ b/osu.Game/Overlays/Music/Playlist.cs @@ -7,16 +7,17 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.Containers; using osuTK; namespace osu.Game.Overlays.Music { - public class Playlist : OsuRearrangeableListContainer + public class Playlist : OsuRearrangeableListContainer> { - public Action RequestSelection; + public Action> RequestSelection; - public readonly Bindable SelectedSet = new Bindable(); + public readonly Bindable> SelectedSet = new Bindable>(); public new MarginPadding Padding { @@ -26,23 +27,23 @@ namespace osu.Game.Overlays.Music public void Filter(FilterCriteria criteria) { - var items = (SearchContainer>)ListContainer; + var items = (SearchContainer>>)ListContainer; foreach (var item in items.OfType()) - item.InSelectedCollection = criteria.Collection?.Beatmaps.Any(b => item.Model.Equals(b.BeatmapSet)) ?? true; + item.InSelectedCollection = criteria.Collection?.Beatmaps.Any(b => item.Model.ID == b.BeatmapSet?.ID) ?? true; items.SearchTerm = criteria.SearchText; } - public BeatmapSetInfo FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); + public ILive FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); - protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapSetInfo item) => new PlaylistItem(item) + protected override OsuRearrangeableListItem> CreateOsuDrawable(ILive item) => new PlaylistItem(item) { SelectedSet = { BindTarget = SelectedSet }, RequestSelection = set => RequestSelection?.Invoke(set) }; - protected override FillFlowContainer> CreateListFillFlowContainer() => new SearchContainer> + protected override FillFlowContainer>> CreateListFillFlowContainer() => new SearchContainer>> { Spacing = new Vector2(0, 3), LayoutDuration = 200, diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index b31a03a64d..3f82580bfb 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -10,17 +10,18 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK.Graphics; namespace osu.Game.Overlays.Music { - public class PlaylistItem : OsuRearrangeableListItem, IFilterable + public class PlaylistItem : OsuRearrangeableListItem>, IFilterable { - public readonly Bindable SelectedSet = new Bindable(); + public readonly Bindable> SelectedSet = new Bindable>(); - public Action RequestSelection; + public Action> RequestSelection; private TextFlowContainer text; private ITextPart titlePart; @@ -28,7 +29,7 @@ namespace osu.Game.Overlays.Music [Resolved] private OsuColour colours { get; set; } - public PlaylistItem(BeatmapSetInfo item) + public PlaylistItem(ILive item) : base(item) { Padding = new MarginPadding { Left = 5 }; @@ -44,34 +45,37 @@ namespace osu.Game.Overlays.Music { base.LoadComplete(); - var metadata = Model.Metadata; - - var title = new RomanisableString(metadata.TitleUnicode, metadata.Title); - var artist = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); - - titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)); - titlePart.DrawablePartsRecreated += _ => updateSelectionState(true); - - text.AddText(@" "); // to separate the title from the artist. - text.AddText(artist, sprite => + Model.PerformRead(m => { - sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); - sprite.Colour = colours.Gray9; - sprite.Padding = new MarginPadding { Top = 1 }; + var metadata = m.Metadata; + + var title = new RomanisableString(metadata.TitleUnicode, metadata.Title); + var artist = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); + + titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)); + titlePart.DrawablePartsRecreated += _ => updateSelectionState(true); + + text.AddText(@" "); // to separate the title from the artist. + text.AddText(artist, sprite => + { + sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); + sprite.Colour = colours.Gray9; + sprite.Padding = new MarginPadding { Top = 1 }; + }); + + SelectedSet.BindValueChanged(set => + { + bool newSelected = set.NewValue?.Equals(Model) == true; + + if (newSelected == selected) + return; + + selected = newSelected; + updateSelectionState(false); + }); + + updateSelectionState(true); }); - - SelectedSet.BindValueChanged(set => - { - bool newSelected = set.NewValue?.Equals(Model) == true; - - if (newSelected == selected) - return; - - selected = newSelected; - updateSelectionState(false); - }); - - updateSelectionState(true); } private bool selected; @@ -109,7 +113,7 @@ namespace osu.Game.Overlays.Music } } - public IEnumerable FilterTerms => Model.Metadata.GetSearchableTerms(); + public IEnumerable FilterTerms => Model.PerformRead(m => m.Metadata.GetSearchableTerms()); private bool matchingFilter = true; diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index ffddf168ed..4b10bb779e 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -24,9 +24,9 @@ namespace osu.Game.Overlays.Music private const float transition_duration = 600; private const float playlist_height = 510; - public IBindableList BeatmapSets => beatmapSets; + public IBindableList> BeatmapSets => beatmapSets; - private readonly BindableList beatmapSets = new BindableList(); + private readonly BindableList> beatmapSets = new BindableList>(); private readonly Bindable beatmap = new Bindable(); @@ -85,13 +85,16 @@ namespace osu.Game.Overlays.Music filter.Search.OnCommit += (sender, newText) => { - BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps.FirstOrDefault(); - - if (toSelect != null) + list.FirstVisibleSet.PerformRead(set => { - beatmap.Value = beatmaps.GetWorkingBeatmap(toSelect); - beatmap.Value.Track.Restart(); - } + BeatmapInfo toSelect = set.Beatmaps.FirstOrDefault(); + + if (toSelect != null) + { + beatmap.Value = beatmaps.GetWorkingBeatmap(toSelect); + beatmap.Value.Track.Restart(); + } + }); }; } @@ -104,7 +107,7 @@ namespace osu.Game.Overlays.Music beatmapSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending), beatmapsChanged); list.Items.BindTo(beatmapSets); - beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo, true); + beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true); } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) @@ -113,12 +116,12 @@ namespace osu.Game.Overlays.Music { beatmapSets.Clear(); // must use AddRange to avoid RearrangeableList sort overhead per add op. - beatmapSets.AddRange(sender); + beatmapSets.AddRange(sender.ToLive(realm)); return; } foreach (int i in changes.InsertedIndices) - beatmapSets.Insert(i, sender[i]); + beatmapSets.Insert(i, sender[i].ToLive(realm)); foreach (int i in changes.DeletedIndices.OrderByDescending(i => i)) beatmapSets.RemoveAt(i); @@ -141,16 +144,19 @@ namespace osu.Game.Overlays.Music this.FadeOut(transition_duration); } - private void itemSelected(BeatmapSetInfo set) + private void itemSelected(ILive beatmapSet) { - if (set.Equals((beatmap.Value?.BeatmapSetInfo))) + beatmapSet.PerformRead(set => { - beatmap.Value?.Track.Seek(0); - return; - } + if (set.Equals((beatmap.Value?.BeatmapSetInfo))) + { + beatmap.Value?.Track.Seek(0); + return; + } - beatmap.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First()); - beatmap.Value.Track.Restart(); + beatmap.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First()); + beatmap.Value.Track.Restart(); + }); } protected override void Dispose(bool isDisposing) From d76822b6857dbad12737662352157d1bb4b6ff69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jan 2022 18:32:57 +0900 Subject: [PATCH 148/587] Avoid creating realm contexts or refetching when accessing `RealmLive` from the update thread --- osu.Game/Database/RealmLive.cs | 38 +++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index f06ba1eff9..c572d52cc6 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Development; using Realms; @@ -22,7 +23,9 @@ namespace osu.Game.Database /// /// The original live data used to create this instance. /// - private readonly T data; + private T data; + + private bool dataIsFromUpdateThread; private readonly RealmAccess realm; @@ -37,6 +40,7 @@ namespace osu.Game.Database this.realm = realm; ID = data.ID; + dataIsFromUpdateThread = ThreadSafety.IsUpdateThread; } /// @@ -51,7 +55,17 @@ namespace osu.Game.Database return; } - realm.Run(r => perform(retrieveFromID(r, ID))); + realm.Run(r => + { + if (ThreadSafety.IsUpdateThread) + { + ensureDataIsFromUpdateThread(); + perform(data); + return; + } + + perform(retrieveFromID(r, ID)); + }); } /// @@ -63,6 +77,12 @@ namespace osu.Game.Database if (!IsManaged) return perform(data); + if (ThreadSafety.IsUpdateThread) + { + ensureDataIsFromUpdateThread(); + return perform(data); + } + return realm.Run(r => { var returnData = perform(retrieveFromID(r, ID)); @@ -101,10 +121,22 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads"); - return realm.Realm.Find(ID); + ensureDataIsFromUpdateThread(); + return data; } } + private void ensureDataIsFromUpdateThread() + { + Debug.Assert(ThreadSafety.IsUpdateThread); + + if (dataIsFromUpdateThread) + return; + + dataIsFromUpdateThread = true; + data = retrieveFromID(realm.Realm, ID); + } + private T retrieveFromID(Realm realm, Guid id) { var found = realm.Find(ID); From 56b06f34f01818063f18cf5d7d1c952f328f7dc9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 02:10:14 +0900 Subject: [PATCH 149/587] Fix `RealmLive` not refetching if update thread context was closed at some point --- osu.Game/Database/RealmLive.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index c572d52cc6..d420c3b5c7 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -130,7 +130,7 @@ namespace osu.Game.Database { Debug.Assert(ThreadSafety.IsUpdateThread); - if (dataIsFromUpdateThread) + if (dataIsFromUpdateThread && !data.Realm.IsClosed) return; dataIsFromUpdateThread = true; From c7947b34890aa64297cc22139ec9f6b683c81492 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 12:42:24 +0900 Subject: [PATCH 150/587] Add statistics for `Live` usage --- osu.Game/Database/RealmLive.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index d420c3b5c7..76a17e860f 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Development; +using osu.Framework.Statistics; using Realms; #nullable enable @@ -14,7 +15,7 @@ namespace osu.Game.Database /// Provides a method of working with realm objects over longer application lifetimes. /// /// The underlying object type. - public class RealmLive : ILive where T : RealmObject, IHasGuidPrimaryKey + public class RealmLive : RealmLive, ILive where T : RealmObject, IHasGuidPrimaryKey { public Guid ID { get; } @@ -65,6 +66,7 @@ namespace osu.Game.Database } perform(retrieveFromID(r, ID)); + USAGE_ASYNC.Value++; }); } @@ -86,6 +88,7 @@ namespace osu.Game.Database return realm.Run(r => { var returnData = perform(retrieveFromID(r, ID)); + USAGE_ASYNC.Value++; if (returnData is RealmObjectBase realmObject && realmObject.IsManaged) throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}."); @@ -108,6 +111,7 @@ namespace osu.Game.Database var transaction = t.Realm.BeginWrite(); perform(t); transaction.Commit(); + WRITES.Value++; }); } @@ -131,10 +135,14 @@ namespace osu.Game.Database Debug.Assert(ThreadSafety.IsUpdateThread); if (dataIsFromUpdateThread && !data.Realm.IsClosed) + { + USAGE_UPDATE_IMMEDIATE.Value++; return; + } dataIsFromUpdateThread = true; data = retrieveFromID(realm.Realm, ID); + USAGE_UPDATE_REFETCH.Value++; } private T retrieveFromID(Realm realm, Guid id) @@ -157,4 +165,12 @@ namespace osu.Game.Database public override string ToString() => PerformRead(i => i.ToString()); } + + public class RealmLive + { + protected static readonly GlobalStatistic WRITES = GlobalStatistics.Get(@"Realm", @"Live writes"); + protected static readonly GlobalStatistic USAGE_UPDATE_IMMEDIATE = GlobalStatistics.Get(@"Realm", @"Live update read (fast)"); + protected static readonly GlobalStatistic USAGE_UPDATE_REFETCH = GlobalStatistics.Get(@"Realm", @"Live update read (slow)"); + protected static readonly GlobalStatistic USAGE_ASYNC = GlobalStatistics.Get(@"Realm", @"Live async read"); + } } From 7ca73f7e6def6c7ae3447ba79fc0b475984c9b74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 12:51:09 +0900 Subject: [PATCH 151/587] Don't auto-unblock realm when user has manually pressed unblock button --- .../Overlays/Settings/Sections/DebugSettings/MemorySettings.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index 3b94cae171..f26326a220 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -73,6 +73,9 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings void unblock() { + if (token == null) + return; + token?.Dispose(); token = null; From d37c3c463e5b82aaa7084e1e748ead6651eb2ba9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 13:29:12 +0900 Subject: [PATCH 152/587] Move statistics to static class --- osu.Game/Database/RealmLive.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 76a17e860f..13b9bc2704 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -15,7 +15,7 @@ namespace osu.Game.Database /// Provides a method of working with realm objects over longer application lifetimes. /// /// The underlying object type. - public class RealmLive : RealmLive, ILive where T : RealmObject, IHasGuidPrimaryKey + public class RealmLive : ILive where T : RealmObject, IHasGuidPrimaryKey { public Guid ID { get; } @@ -66,7 +66,7 @@ namespace osu.Game.Database } perform(retrieveFromID(r, ID)); - USAGE_ASYNC.Value++; + RealmLiveStatistics.USAGE_ASYNC.Value++; }); } @@ -88,7 +88,7 @@ namespace osu.Game.Database return realm.Run(r => { var returnData = perform(retrieveFromID(r, ID)); - USAGE_ASYNC.Value++; + RealmLiveStatistics.USAGE_ASYNC.Value++; if (returnData is RealmObjectBase realmObject && realmObject.IsManaged) throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}."); @@ -111,7 +111,7 @@ namespace osu.Game.Database var transaction = t.Realm.BeginWrite(); perform(t); transaction.Commit(); - WRITES.Value++; + RealmLiveStatistics.WRITES.Value++; }); } @@ -136,13 +136,13 @@ namespace osu.Game.Database if (dataIsFromUpdateThread && !data.Realm.IsClosed) { - USAGE_UPDATE_IMMEDIATE.Value++; + RealmLiveStatistics.USAGE_UPDATE_IMMEDIATE.Value++; return; } dataIsFromUpdateThread = true; data = retrieveFromID(realm.Realm, ID); - USAGE_UPDATE_REFETCH.Value++; + RealmLiveStatistics.USAGE_UPDATE_REFETCH.Value++; } private T retrieveFromID(Realm realm, Guid id) @@ -166,11 +166,11 @@ namespace osu.Game.Database public override string ToString() => PerformRead(i => i.ToString()); } - public class RealmLive + internal static class RealmLiveStatistics { - protected static readonly GlobalStatistic WRITES = GlobalStatistics.Get(@"Realm", @"Live writes"); - protected static readonly GlobalStatistic USAGE_UPDATE_IMMEDIATE = GlobalStatistics.Get(@"Realm", @"Live update read (fast)"); - protected static readonly GlobalStatistic USAGE_UPDATE_REFETCH = GlobalStatistics.Get(@"Realm", @"Live update read (slow)"); - protected static readonly GlobalStatistic USAGE_ASYNC = GlobalStatistics.Get(@"Realm", @"Live async read"); + public static readonly GlobalStatistic WRITES = GlobalStatistics.Get(@"Realm", @"Live writes"); + public static readonly GlobalStatistic USAGE_UPDATE_IMMEDIATE = GlobalStatistics.Get(@"Realm", @"Live update read (fast)"); + public static readonly GlobalStatistic USAGE_UPDATE_REFETCH = GlobalStatistics.Get(@"Realm", @"Live update read (slow)"); + public static readonly GlobalStatistic USAGE_ASYNC = GlobalStatistics.Get(@"Realm", @"Live async read"); } } From cd71ec0edd6576384418b45683ae3615dbdca669 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 13:37:33 +0900 Subject: [PATCH 153/587] Remove `ILive<>` interface (and use `abstract Live<>` instead) --- .../Database/BeatmapImporterTests.cs | 8 +++--- osu.Game.Tests/Database/RealmLiveTests.cs | 16 +++++------ ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 4 +-- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 8 +++--- .../Gameplay/TestSceneReplayDownloadButton.cs | 2 +- .../TestSceneDrawableRoomPlaylist.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 14 +++++----- osu.Game/Database/IModelImporter.cs | 10 +++---- osu.Game/Database/IPostImports.cs | 4 +-- osu.Game/Database/LegacyModelImporter.cs | 2 +- osu.Game/Database/{ILive.cs => Live.cs} | 27 +++++++++++++------ osu.Game/Database/RealmLive.cs | 20 +++++--------- osu.Game/Database/RealmLiveUnmanaged.cs | 27 ++++++++----------- osu.Game/Database/RealmObjectExtensions.cs | 12 ++++----- osu.Game/OsuGame.cs | 4 +-- .../Overlays/Settings/Sections/SkinSection.cs | 14 +++++----- osu.Game/Scoring/ScoreManager.cs | 10 +++---- osu.Game/Skinning/Skin.cs | 2 +- osu.Game/Skinning/SkinManager.cs | 14 +++++----- osu.Game/Stores/RealmArchiveModelImporter.cs | 16 +++++------ 20 files changed, 108 insertions(+), 108 deletions(-) rename osu.Game/Database/{ILive.cs => Live.cs} (65%) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 69dd2d930a..2c7d0211a0 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Database using (var importer = new BeatmapModelManager(realm, storage)) using (new RulesetStore(realm, storage)) { - ILive? beatmapSet; + Live? beatmapSet; using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) beatmapSet = await importer.Import(reader); @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Database using (var importer = new BeatmapModelManager(realm, storage)) using (new RulesetStore(realm, storage)) { - ILive? beatmapSet; + Live? beatmapSet; using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) beatmapSet = await importer.Import(reader); @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Database using (var importer = new BeatmapModelManager(realm, storage)) using (new RulesetStore(realm, storage)) { - ILive? imported; + Live? imported; using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) imported = await importer.Import(reader); @@ -219,7 +219,7 @@ namespace osu.Game.Tests.Database string? tempPath = TestResources.GetTestBeatmapForImport(); - ILive? importedSet; + Live? importedSet; using (var stream = File.OpenRead(tempPath)) { diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 2e3f708f79..3f81b36378 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -23,9 +23,9 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realm, _) => { - ILive beatmap = realm.Run(r => r.Write(_ => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realm)); + Live beatmap = realm.Run(r => r.Write(_ => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realm)); - ILive beatmap2 = realm.Run(r => r.All().First().ToLive(realm)); + Live beatmap2 = realm.Run(r => r.All().First().ToLive(realm)); Assert.AreEqual(beatmap, beatmap2); }); @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Database { var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); - ILive? liveBeatmap = null; + Live? liveBeatmap = null; realm.Run(r => { @@ -100,7 +100,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realm, _) => { - ILive? liveBeatmap = null; + Live? liveBeatmap = null; Task.Factory.StartNew(() => { realm.Run(threadContext => @@ -129,7 +129,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realm, _) => { - ILive? liveBeatmap = null; + Live? liveBeatmap = null; Task.Factory.StartNew(() => { realm.Run(threadContext => @@ -170,7 +170,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realm, _) => { - ILive? liveBeatmap = null; + Live? liveBeatmap = null; Task.Factory.StartNew(() => { @@ -209,7 +209,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realm, _) => { - ILive? liveBeatmap = null; + Live? liveBeatmap = null; Task.Factory.StartNew(() => { realm.Run(threadContext => @@ -242,7 +242,7 @@ namespace osu.Game.Tests.Database realm.RegisterCustomSubscription(outerRealm => { outerRealm.All().QueryAsyncWithNotifications(gotChange); - ILive? liveBeatmap = null; + Live? liveBeatmap = null; Task.Factory.StartNew(() => { diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 95c15367aa..f9161816e7 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -164,7 +164,7 @@ namespace osu.Game.Tests.Online { public TaskCompletionSource AllowImport = new TaskCompletionSource(); - public ILive CurrentImport { get; private set; } + public Live CurrentImport { get; private set; } public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap) @@ -186,7 +186,7 @@ namespace osu.Game.Tests.Online this.testBeatmapManager = testBeatmapManager; } - public override ILive Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public override Live Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { testBeatmapManager.AllowImport.Task.WaitSafely(); return (testBeatmapManager.CurrentImport = base.Import(item, archive, lowPriority, cancellationToken)); diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 3f063264e0..9b0facd625 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -235,7 +235,7 @@ namespace osu.Game.Tests.Skins.IO #endregion - private void assertCorrectMetadata(ILive import1, string name, string creator, OsuGameBase osu) + private void assertCorrectMetadata(Live import1, string name, string creator, OsuGameBase osu) { import1.PerformRead(i => { @@ -250,7 +250,7 @@ namespace osu.Game.Tests.Skins.IO }); } - private void assertImportedBoth(ILive import1, ILive import2) + private void assertImportedBoth(Live import1, Live import2) { import1.PerformRead(i1 => import2.PerformRead(i2 => { @@ -260,7 +260,7 @@ namespace osu.Game.Tests.Skins.IO })); } - private void assertImportedOnce(ILive import1, ILive import2) + private void assertImportedOnce(Live import1, Live import2) { import1.PerformRead(i1 => import2.PerformRead(i2 => { @@ -334,7 +334,7 @@ namespace osu.Game.Tests.Skins.IO } } - private async Task> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) + private async Task> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) { var skinManager = osu.Dependencies.Get(); return await skinManager.Import(archive); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index a12171401a..8b7e1c4e58 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestScoreImportThenDelete() { - ILive imported = null; + Live imported = null; AddStep("create button without replay", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 92accb0cd1..5c8c90e166 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestDownloadButtonHiddenWhenBeatmapExists() { var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo; - ILive imported = null; + Live imported = null; Debug.Assert(beatmap.BeatmapSet != null); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a1c1982f00..414b7cd12b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -182,7 +182,7 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public ILive? QueryBeatmapSet(Expression> query) + public Live? QueryBeatmapSet(Expression> query) { return realm.Run(r => r.All().FirstOrDefault(query)?.ToLive(realm)); } @@ -279,22 +279,22 @@ namespace osu.Game.Beatmaps return beatmapModelManager.Import(tasks); } - public Task>> Import(ProgressNotification notification, params ImportTask[] 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) + 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) + public Task?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(archive, lowPriority, cancellationToken); } - public ILive? Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public Live? Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken); } @@ -323,7 +323,7 @@ namespace osu.Game.Beatmaps return workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); } - public WorkingBeatmap GetWorkingBeatmap(ILive? importedBeatmap) + public WorkingBeatmap GetWorkingBeatmap(Live? importedBeatmap) { WorkingBeatmap working = workingBeatmapCache.GetWorkingBeatmap(null); @@ -367,7 +367,7 @@ namespace osu.Game.Beatmaps #region Implementation of IPostImports - public Action>>? PostImport + public Action>>? PostImport { set => beatmapModelManager.PostImport = value; } diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index 3047a1d30a..90df13477e 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -16,9 +16,9 @@ namespace osu.Game.Database /// /// The model type. public interface IModelImporter : IPostNotifications, IPostImports, ICanAcceptFiles - where TModel : class + where TModel : class, IHasGuidPrimaryKey { - Task>> Import(ProgressNotification notification, params ImportTask[] tasks); + Task>> Import(ProgressNotification notification, params ImportTask[] tasks); /// /// Import one from the filesystem and delete the file on success. @@ -28,7 +28,7 @@ namespace osu.Game.Database /// 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); + Task?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); /// /// Silently import an item from an . @@ -36,7 +36,7 @@ namespace osu.Game.Database /// 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); + Task?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); /// /// Silently import an item from a . @@ -45,7 +45,7 @@ namespace osu.Game.Database /// An optional archive to use for model population. /// Whether this is a low priority import. /// An optional cancellation token. - ILive? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); + Live? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); /// /// A user displayable name for the model type associated with this manager. diff --git a/osu.Game/Database/IPostImports.cs b/osu.Game/Database/IPostImports.cs index adb3a7108d..6f047098da 100644 --- a/osu.Game/Database/IPostImports.cs +++ b/osu.Game/Database/IPostImports.cs @@ -9,11 +9,11 @@ using System.Collections.Generic; namespace osu.Game.Database { public interface IPostImports - where TModel : class + where TModel : class, IHasGuidPrimaryKey { /// /// Fired when the user requests to view the resulting import. /// - public Action>>? PostImport { set; } + public Action>>? PostImport { set; } } } diff --git a/osu.Game/Database/LegacyModelImporter.cs b/osu.Game/Database/LegacyModelImporter.cs index dacb7327ea..d85fb5aab2 100644 --- a/osu.Game/Database/LegacyModelImporter.cs +++ b/osu.Game/Database/LegacyModelImporter.cs @@ -14,7 +14,7 @@ namespace osu.Game.Database /// A class which handles importing legacy user data of a single type from osu-stable. /// public abstract class LegacyModelImporter - where TModel : class + where TModel : class, IHasGuidPrimaryKey { /// /// The relative path from osu-stable's data directory to import items from. diff --git a/osu.Game/Database/ILive.cs b/osu.Game/Database/Live.cs similarity index 65% rename from osu.Game/Database/ILive.cs rename to osu.Game/Database/Live.cs index 3011754bc1..6256902e17 100644 --- a/osu.Game/Database/ILive.cs +++ b/osu.Game/Database/Live.cs @@ -3,39 +3,41 @@ using System; +#nullable enable + namespace osu.Game.Database { /// /// A wrapper to provide access to database backed classes in a thread-safe manner. /// /// The databased type. - public interface ILive : IEquatable> - where T : class // TODO: Add IHasGuidPrimaryKey once we don't need EF support any more. + public abstract class Live : IEquatable> + where T : class, IHasGuidPrimaryKey { - Guid ID { get; } + public Guid ID { get; } /// /// Perform a read operation on this live object. /// /// The action to perform. - void PerformRead(Action perform); + public abstract void PerformRead(Action perform); /// /// Perform a read operation on this live object. /// /// The action to perform. - TReturn PerformRead(Func perform); + public abstract TReturn PerformRead(Func perform); /// /// Perform a write operation on this live object. /// /// The action to perform. - void PerformWrite(Action perform); + public abstract void PerformWrite(Action perform); /// /// Whether this instance is tracking data which is managed by the database backing. /// - bool IsManaged { get; } + public abstract bool IsManaged { get; } /// /// Resolve the value of this instance on the update thread. @@ -43,6 +45,15 @@ namespace osu.Game.Database /// /// After resolving, the data should not be passed between threads. /// - T Value { get; } + public abstract T Value { get; } + + protected Live(Guid id) + { + ID = id; + } + + public bool Equals(Live? other) => ID == other?.ID; + + public override string ToString() => PerformRead(i => i.ToString()); } } diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 13b9bc2704..186e801425 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -15,11 +15,9 @@ namespace osu.Game.Database /// Provides a method of working with realm objects over longer application lifetimes. /// /// The underlying object type. - public class RealmLive : ILive where T : RealmObject, IHasGuidPrimaryKey + public class RealmLive : Live where T : RealmObject, IHasGuidPrimaryKey { - public Guid ID { get; } - - public bool IsManaged => data.IsManaged; + public override bool IsManaged => data.IsManaged; /// /// The original live data used to create this instance. @@ -36,11 +34,11 @@ namespace osu.Game.Database /// The realm data. /// The realm factory the data was sourced from. May be null for an unmanaged object. public RealmLive(T data, RealmAccess realm) + : base(data.ID) { this.data = data; this.realm = realm; - ID = data.ID; dataIsFromUpdateThread = ThreadSafety.IsUpdateThread; } @@ -48,7 +46,7 @@ namespace osu.Game.Database /// Perform a read operation on this live object. /// /// The action to perform. - public void PerformRead(Action perform) + public override void PerformRead(Action perform) { if (!IsManaged) { @@ -74,7 +72,7 @@ namespace osu.Game.Database /// Perform a read operation on this live object. /// /// The action to perform. - public TReturn PerformRead(Func perform) + public override TReturn PerformRead(Func perform) { if (!IsManaged) return perform(data); @@ -101,7 +99,7 @@ namespace osu.Game.Database /// Perform a write operation on this live object. /// /// The action to perform. - public void PerformWrite(Action perform) + public override void PerformWrite(Action perform) { if (!IsManaged) throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value"); @@ -115,7 +113,7 @@ namespace osu.Game.Database }); } - public T Value + public override T Value { get { @@ -160,10 +158,6 @@ namespace osu.Game.Database return found; } - - public bool Equals(ILive? other) => ID == other?.ID; - - public override string ToString() => PerformRead(i => i.ToString()); } internal static class RealmLiveStatistics diff --git a/osu.Game/Database/RealmLiveUnmanaged.cs b/osu.Game/Database/RealmLiveUnmanaged.cs index 97f2faa656..1080f3b8c7 100644 --- a/osu.Game/Database/RealmLiveUnmanaged.cs +++ b/osu.Game/Database/RealmLiveUnmanaged.cs @@ -13,13 +13,19 @@ namespace osu.Game.Database /// Usually used for testing purposes where the instance is never required to be managed. /// /// The underlying object type. - public class RealmLiveUnmanaged : ILive where T : RealmObjectBase, IHasGuidPrimaryKey + public class RealmLiveUnmanaged : Live where T : RealmObjectBase, IHasGuidPrimaryKey { + /// + /// The original live data used to create this instance. + /// + public override T Value { get; } + /// /// Construct a new instance of live realm data. /// /// The realm data. public RealmLiveUnmanaged(T data) + : base(data.ID) { if (data.IsManaged) throw new InvalidOperationException($"Cannot use {nameof(RealmLiveUnmanaged)} with managed instances"); @@ -27,23 +33,12 @@ namespace osu.Game.Database Value = data; } - public bool Equals(ILive? other) => ID == other?.ID; + public override void PerformRead(Action perform) => perform(Value); - public override string ToString() => Value.ToString(); + public override TReturn PerformRead(Func perform) => perform(Value); - public Guid ID => Value.ID; + public override void PerformWrite(Action perform) => throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value"); - public void PerformRead(Action perform) => perform(Value); - - public TReturn PerformRead(Func perform) => perform(Value); - - public void PerformWrite(Action perform) => throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value"); - - public bool IsManaged => false; - - /// - /// The original live data used to create this instance. - /// - public T Value { get; } + public override bool IsManaged => false; } } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index d4f8978ac5..dba8633f53 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -204,25 +204,25 @@ namespace osu.Game.Database private static void copyChangesToRealm(T source, T destination) where T : RealmObjectBase => write_mapper.Map(source, destination); - public static List> ToLiveUnmanaged(this IEnumerable realmList) + public static List> ToLiveUnmanaged(this IEnumerable realmList) where T : RealmObject, IHasGuidPrimaryKey { - return realmList.Select(l => new RealmLiveUnmanaged(l)).Cast>().ToList(); + return realmList.Select(l => new RealmLiveUnmanaged(l)).Cast>().ToList(); } - public static ILive ToLiveUnmanaged(this T realmObject) + public static Live ToLiveUnmanaged(this T realmObject) where T : RealmObject, IHasGuidPrimaryKey { return new RealmLiveUnmanaged(realmObject); } - public static List> ToLive(this IEnumerable realmList, RealmAccess realm) + public static List> ToLive(this IEnumerable realmList, RealmAccess realm) where T : RealmObject, IHasGuidPrimaryKey { - return realmList.Select(l => new RealmLive(l, realm)).Cast>().ToList(); + return realmList.Select(l => new RealmLive(l, realm)).Cast>().ToList(); } - public static ILive ToLive(this T realmObject, RealmAccess realm) + public static Live ToLive(this T realmObject, RealmAccess realm) where T : RealmObject, IHasGuidPrimaryKey { return new RealmLive(realmObject, realm); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5b3abc54d3..c2e1b25d94 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -249,7 +249,7 @@ namespace osu.Game SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString(); configSkin.ValueChanged += skinId => { - ILive skinInfo = null; + Live skinInfo = null; if (Guid.TryParse(skinId.NewValue, out var guid)) skinInfo = SkinManager.Query(s => s.ID == guid); @@ -439,7 +439,7 @@ namespace osu.Game /// public void PresentBeatmap(IBeatmapSetInfo beatmap, Predicate difficultyCriteria = null) { - ILive databasedSet = null; + Live databasedSet = null; if (beatmap.OnlineID > 0) databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineID == beatmap.OnlineID); diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 8ab296c0a8..0846c023c1 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -32,16 +32,16 @@ namespace osu.Game.Overlays.Settings.Sections Icon = FontAwesome.Solid.PaintBrush }; - private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLiveUnmanaged() }; + private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLiveUnmanaged() }; private readonly Bindable configBindable = new Bindable(); - private static readonly ILive random_skin_info = new SkinInfo + private static readonly Live random_skin_info = new SkinInfo { ID = SkinInfo.RANDOM_SKIN, Name = "", }.ToLiveUnmanaged(); - private List> skinItems; + private List> skinItems; [Resolved] private SkinManager skins { get; set; } @@ -118,7 +118,7 @@ namespace osu.Game.Overlays.Settings.Sections private void updateSelectedSkinFromConfig() { - ILive skin = null; + Live skin = null; if (Guid.TryParse(configBindable.Value, out var configId)) skin = skinDropdown.Items.FirstOrDefault(s => s.ID == configId); @@ -144,13 +144,13 @@ namespace osu.Game.Overlays.Settings.Sections realmSubscription?.Dispose(); } - private class SkinSettingsDropdown : SettingsDropdown> + private class SkinSettingsDropdown : SettingsDropdown> { - protected override OsuDropdown> CreateDropdown() => new SkinDropdownControl(); + protected override OsuDropdown> CreateDropdown() => new SkinDropdownControl(); private class SkinDropdownControl : DropdownControl { - protected override LocalisableString GenerateItemText(ILive item) => item.ToString(); + protected override LocalisableString GenerateItemText(Live item) => item.ToString(); } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 3a842a048a..8f665224ee 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -293,22 +293,22 @@ namespace osu.Game.Scoring public IEnumerable HandledExtensions => scoreModelManager.HandledExtensions; - public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) { return scoreModelManager.Import(notification, tasks); } - public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + 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) + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { return scoreModelManager.Import(archive, lowPriority, cancellationToken); } - public ILive Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public Live Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return scoreModelManager.Import(item, archive, lowPriority, cancellationToken); } @@ -322,7 +322,7 @@ namespace osu.Game.Scoring #region Implementation of IPresentImports - public Action>> PostImport + public Action>> PostImport { set => scoreModelManager.PostImport = value; } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 3685a26e26..931bdfed48 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -24,7 +24,7 @@ namespace osu.Game.Skinning { public abstract class Skin : IDisposable, ISkin { - public readonly ILive SkinInfo; + public readonly Live SkinInfo; private readonly IStorageResourceProvider resources; public SkinConfiguration Configuration { get; set; } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 47c7bc060a..06bd0abc9f 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -47,7 +47,7 @@ namespace osu.Game.Skinning public readonly Bindable CurrentSkin = new Bindable(); - public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()) + public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()) { Default = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged() }; @@ -176,7 +176,7 @@ namespace osu.Game.Skinning /// /// The query. /// The first result for the provided query, or null if no results were found. - public ILive Query(Expression> query) + public Live Query(Expression> query) { return realm.Run(r => r.All().FirstOrDefault(query)?.ToLive(realm)); } @@ -245,7 +245,7 @@ namespace osu.Game.Skinning set => skinModelManager.PostNotification = value; } - public Action>> PostImport + public Action>> PostImport { set => skinModelManager.PostImport = value; } @@ -262,22 +262,22 @@ namespace osu.Game.Skinning public IEnumerable HandledExtensions => skinModelManager.HandledExtensions; - public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) { return skinModelManager.Import(notification, tasks); } - public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { return skinModelManager.Import(task, lowPriority, cancellationToken); } - public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { return skinModelManager.Import(archive, lowPriority, cancellationToken); } - public ILive Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public Live Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return skinModelManager.Import(item, archive, lowPriority, cancellationToken); } diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 43c1c7c888..3011bc0320 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -64,7 +64,7 @@ namespace osu.Game.Stores /// /// Fired when the user requests to view the resulting import. /// - public Action>>? PostImport { get; set; } + public Action>>? PostImport { get; set; } /// /// Set an endpoint for notifications to be posted to. @@ -104,7 +104,7 @@ namespace osu.Game.Stores return Import(notification, tasks); } - public async Task>> Import(ProgressNotification notification, params ImportTask[] tasks) + public async Task>> Import(ProgressNotification notification, params ImportTask[] tasks) { if (tasks.Length == 0) { @@ -118,7 +118,7 @@ namespace osu.Game.Stores int current = 0; - var imported = new List>(); + var imported = new List>(); bool isLowPriorityImport = tasks.Length > low_priority_import_batch_size; @@ -196,11 +196,11 @@ namespace osu.Game.Stores /// Whether this is a low priority import. /// An optional cancellation token. /// The imported model, if successful. - public 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(); - ILive? import; + Live? import; using (ArchiveReader reader = task.GetReader()) import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false); @@ -227,7 +227,7 @@ namespace osu.Game.Stores /// The archive to be imported. /// Whether this is a low priority import. /// An optional cancellation token. - public async Task?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + public async Task?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -320,7 +320,7 @@ namespace osu.Game.Stores /// An optional archive to use for model population. /// Whether this is a low priority import. /// An optional cancellation token. - public virtual ILive? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public virtual Live? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return Realm.Run(realm => { @@ -416,7 +416,7 @@ namespace osu.Game.Stores throw; } - return (ILive?)item.ToLive(Realm); + return (Live?)item.ToLive(Realm); }); } From 064468faada4ba6af341296f634978e774bcec03 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jan 2022 11:31:05 +0300 Subject: [PATCH 154/587] Refactor editor saving test scene for scalability --- .../Visual/Editing/TestSceneEditorSaving.cs | 152 ++++++++++-------- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- .../Tests/Visual/EditorSavingTestScene.cs | 67 ++++++++ 3 files changed, 154 insertions(+), 67 deletions(-) create mode 100644 osu.Game/Tests/Visual/EditorSavingTestScene.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 96f815621c..58daab1ce2 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -4,97 +4,117 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Input; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Setup; -using osu.Game.Screens.Menu; -using osu.Game.Screens.Select; +using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { - public class TestSceneEditorSaving : OsuGameTestScene + public class TestSceneEditorSaving : EditorSavingTestScene { - 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() + public void TestMetadata() { - AddStep("set default beatmap", () => Game.Beatmap.SetDefault()); - - PushAndConfirm(() => new EditorLoader()); - - AddUntilStep("wait for editor load", () => editor?.IsLoaded == true); - - AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - - // We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten. - - AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); - AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - - AddStep("Set beat divisor", () => editor.Dependencies.Get().Value = 16); - AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7); AddStep("Set artist and title", () => { - editorBeatmap.BeatmapInfo.Metadata.Artist = "artist"; - editorBeatmap.BeatmapInfo.Metadata.Title = "title"; + EditorBeatmap.BeatmapInfo.Metadata.Artist = "artist"; + EditorBeatmap.BeatmapInfo.Metadata.Title = "title"; }); - AddStep("Set author", () => editorBeatmap.BeatmapInfo.Metadata.Author.Username = "author"); - AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty"); + AddStep("Set author", () => EditorBeatmap.BeatmapInfo.Metadata.Author.Username = "author"); + AddStep("Set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = "difficulty"); - AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + SaveEditor(); + AddAssert("Beatmap has correct metadata", () => EditorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && EditorBeatmap.BeatmapInfo.Metadata.Title == "title"); + AddAssert("Beatmap has correct author", () => EditorBeatmap.BeatmapInfo.Metadata.Author.Username == "author"); + AddAssert("Beatmap has correct difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "difficulty"); + AddAssert("Beatmap has correct .osu file path", () => EditorBeatmap.BeatmapInfo.Path == "artist - title (author) [difficulty].osu"); + + ReloadEditorToSameBeatmap(); + + AddAssert("Beatmap still has correct metadata", () => EditorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && EditorBeatmap.BeatmapInfo.Metadata.Title == "title"); + AddAssert("Beatmap still has correct author", () => EditorBeatmap.BeatmapInfo.Metadata.Author.Username == "author"); + AddAssert("Beatmap still has correct difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "difficulty"); + AddAssert("Beatmap still has correct .osu file path", () => EditorBeatmap.BeatmapInfo.Path == "artist - title (author) [difficulty].osu"); + } + + [Test] + public void TestConfiguration() + { + double originalTimelineZoom = 0; + double changedTimelineZoom = 0; + + AddStep("Set beat divisor", () => Editor.Dependencies.Get().Value = 16); + AddStep("Set timeline zoom", () => + { + originalTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom; + + var timeline = Editor.ChildrenOfType().Single(); + InputManager.MoveMouseTo(timeline); + InputManager.PressKey(Key.AltLeft); + InputManager.ScrollVerticalBy(15f); + InputManager.ReleaseKey(Key.AltLeft); + }); + + AddAssert("Ensure timeline zoom changed", () => + { + changedTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom; + return !Precision.AlmostEquals(changedTimelineZoom, originalTimelineZoom); + }); + + SaveEditor(); + + AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16); + AddAssert("Beatmap has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); + + ReloadEditorToSameBeatmap(); + + AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16); + AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); + } + + [Test] + public void TestDifficulty() + { + AddStep("Set overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty = 7); + + SaveEditor(); + + AddAssert("Beatmap has correct overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty == 7); + + ReloadEditorToSameBeatmap(); + + AddAssert("Beatmap still has correct overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty == 7); + } + + [Test] + public void TestHitObjectPlacement() + { + AddStep("Add timing point", () => EditorBeatmap.ControlPointInfo.Add(500, new TimingControlPoint())); AddStep("Change to placement mode", () => InputManager.Key(Key.Number2)); AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left)); - checkMutations(); + SaveEditor(); + + AddAssert("Beatmap has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500); // After placement these must be non-default as defaults are read-only. AddAssert("Placed object has non-default control points", () => - editorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT && - editorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT); + EditorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT && + EditorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT); - AddStep("Save", () => InputManager.Keys(PlatformAction.Save)); + ReloadEditorToSameBeatmap(); - checkMutations(); - AddAssert("Beatmap has correct .osu file path", () => editorBeatmap.BeatmapInfo.Path == "artist - title (author) [difficulty].osu"); + AddAssert("Beatmap still has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500); - AddStep("Exit", () => InputManager.Key(Key.Escape)); - - AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); - - Screens.Select.SongSelect songSelect = null; - - PushAndConfirm(() => songSelect = new PlaySongSelect()); - AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded); - - 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); - - checkMutations(); - } - - private void checkMutations() - { - AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); - AddAssert("Beatmap has correct beat divisor", () => editorBeatmap.BeatmapInfo.BeatDivisor == 16); - AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7); - AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title"); - AddAssert("Beatmap has correct author", () => editorBeatmap.BeatmapInfo.Metadata.Author.Username == "author"); - AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty"); + // After placement these must be non-default as defaults are read-only. + AddAssert("Placed object still has non-default control points", () => + EditorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT && + EditorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT); } } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 96254295a6..4e1a34ddbf 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -111,7 +111,7 @@ namespace osu.Game.Beatmaps public int GridSize { get; set; } - public double TimelineZoom { get; set; } + public double TimelineZoom { get; set; } = 1.0; [Ignored] public CountdownType Countdown { get; set; } = CountdownType.Normal; diff --git a/osu.Game/Tests/Visual/EditorSavingTestScene.cs b/osu.Game/Tests/Visual/EditorSavingTestScene.cs new file mode 100644 index 0000000000..72b5d076a5 --- /dev/null +++ b/osu.Game/Tests/Visual/EditorSavingTestScene.cs @@ -0,0 +1,67 @@ +// 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.Input; +using osu.Framework.Testing; +using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Setup; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; +using osuTK.Input; + +namespace osu.Game.Tests.Visual +{ + /// + /// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select. + /// + public class EditorSavingTestScene : OsuGameTestScene + { + protected Editor Editor => Game.ChildrenOfType().FirstOrDefault(); + + protected EditorBeatmap EditorBeatmap => (EditorBeatmap)Editor.Dependencies.Get(typeof(EditorBeatmap)); + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("set default beatmap", () => Game.Beatmap.SetDefault()); + + PushAndConfirm(() => new EditorLoader()); + + AddUntilStep("wait for editor load", () => Editor?.IsLoaded == true); + + AddUntilStep("wait for metadata screen load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + + // We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten. + + AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); + AddUntilStep("Wait for compose mode load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + } + + protected void SaveEditor() + { + AddStep("Save", () => InputManager.Keys(PlatformAction.Save)); + } + + protected void ReloadEditorToSameBeatmap() + { + AddStep("Exit", () => InputManager.Key(Key.Escape)); + + AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + + SongSelect songSelect = null; + + PushAndConfirm(() => songSelect = new PlaySongSelect()); + AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded); + + 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); + } + } +} From de0a7d8501c5c3853755c49cf289a6be666c77a9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jan 2022 12:29:41 +0300 Subject: [PATCH 155/587] Migrate taiko editor saving test scene to `EditorSavingTestScene` --- .../Editor/TestSceneEditorSaving.cs | 91 ------------------- .../Editor/TestSceneTaikoEditorSaving.cs | 38 ++++++++ 2 files changed, 38 insertions(+), 91 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorSaving.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorSaving.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorSaving.cs deleted file mode 100644 index 42ab84714a..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorSaving.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using NUnit.Framework; -using osu.Framework.Input; -using osu.Framework.Testing; -using osu.Framework.Utils; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Taiko.Beatmaps; -using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Setup; -using osu.Game.Screens.Menu; -using osu.Game.Screens.Select; -using osu.Game.Tests.Visual; -using osuTK.Input; - -namespace osu.Game.Rulesets.Taiko.Tests.Editor -{ - public class TestSceneEditorSaving : OsuGameTestScene - { - private Screens.Edit.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. - /// Emphasis is placed on , since taiko has special handling for it to keep compatibility with stable. - /// - [Test] - public void TestNewBeatmapSaveThenLoad() - { - AddStep("set default beatmap", () => Game.Beatmap.SetDefault()); - AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); - - PushAndConfirm(() => new EditorLoader()); - - AddUntilStep("wait for editor load", () => editor?.IsLoaded == true); - - AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - - // We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten. - - AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); - AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - - AddStep("Set slider multiplier", () => editorBeatmap.Difficulty.SliderMultiplier = 2); - AddStep("Set artist and title", () => - { - editorBeatmap.BeatmapInfo.Metadata.Artist = "artist"; - editorBeatmap.BeatmapInfo.Metadata.Title = "title"; - }); - AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty"); - - checkMutations(); - - AddStep("Save", () => InputManager.Keys(PlatformAction.Save)); - - checkMutations(); - - AddStep("Exit", () => 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); - - checkMutations(); - } - - private void checkMutations() - { - AddAssert("Beatmap has correct slider multiplier", () => - { - // we can only assert value correctness on TaikoMultiplierAppliedDifficulty, because that is the final difficulty converted taiko beatmaps use. - // therefore, ensure that we have that difficulty type by calling .CopyFrom(), which is a no-op if the type is already correct. - var taikoDifficulty = new TaikoBeatmapConverter.TaikoMultiplierAppliedDifficulty(); - taikoDifficulty.CopyFrom(editorBeatmap.Difficulty); - return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2); - }); - AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title"); - AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty"); - } - } -} diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs new file mode 100644 index 0000000000..33c2ba532e --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests.Editor +{ + public class TestSceneTaikoEditorSaving : EditorSavingTestScene + { + protected override Ruleset CreateRuleset() => new TaikoRuleset(); + + [Test] + public void TestTaikoSliderMultiplier() + { + AddStep("Set slider multiplier", () => EditorBeatmap.Difficulty.SliderMultiplier = 2); + + SaveEditor(); + + AddAssert("Beatmap has correct slider multiplier", assertTaikoSliderMulitplier); + + ReloadEditorToSameBeatmap(); + + AddAssert("Beatmap still has correct slider multiplier", assertTaikoSliderMulitplier); + + bool assertTaikoSliderMulitplier() + { + // we can only assert value correctness on TaikoMultiplierAppliedDifficulty, because that is the final difficulty converted taiko beatmaps use. + // therefore, ensure that we have that difficulty type by calling .CopyFrom(), which is a no-op if the type is already correct. + var taikoDifficulty = new TaikoBeatmapConverter.TaikoMultiplierAppliedDifficulty(); + taikoDifficulty.CopyFrom(EditorBeatmap.Difficulty); + return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2); + } + } + } +} From 3491b77c8cc95ce752a25434e00f130227dad182 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 14:25:55 +0900 Subject: [PATCH 156/587] Fix `ScoreInfo.RealmUser` not getting deep cloned correctly I'm still not at all happy with the play-to-results flow (with multiple clones), but this will have to do for now. --- osu.Game.Tests/NonVisual/ScoreInfoTest.cs | 4 ++++ osu.Game/Scoring/ScoreInfo.cs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/osu.Game.Tests/NonVisual/ScoreInfoTest.cs b/osu.Game.Tests/NonVisual/ScoreInfoTest.cs index e0acc6d8db..836c978ba2 100644 --- a/osu.Game.Tests/NonVisual/ScoreInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ScoreInfoTest.cs @@ -26,12 +26,16 @@ namespace osu.Game.Tests.NonVisual score.Statistics[HitResult.Good]++; score.Rank = ScoreRank.X; + score.RealmUser.Username = "test"; Assert.That(scoreCopy.Statistics[HitResult.Good], Is.EqualTo(10)); Assert.That(score.Statistics[HitResult.Good], Is.EqualTo(11)); Assert.That(scoreCopy.Rank, Is.EqualTo(ScoreRank.B)); Assert.That(score.Rank, Is.EqualTo(ScoreRank.X)); + + Assert.That(scoreCopy.RealmUser.Username, Is.EqualTo("test")); + Assert.That(score.Rank, Is.Empty); } [Test] diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index a28e16450f..4de1d580dc 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -133,6 +133,11 @@ namespace osu.Game.Scoring var clone = (ScoreInfo)this.Detach().MemberwiseClone(); clone.Statistics = new Dictionary(clone.Statistics); + clone.RealmUser = new RealmUser + { + OnlineID = RealmUser.OnlineID, + Username = RealmUser.Username, + }; return clone; } From 9532454e2a0d691652f40b7e15c3bd3503d9c9b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 15:12:01 +0900 Subject: [PATCH 157/587] Remove `ILive` remnants --- .../UserInterface/TestScenePlaylistOverlay.cs | 6 +++--- osu.Game/Overlays/Music/Playlist.cs | 14 +++++++------- osu.Game/Overlays/Music/PlaylistItem.cs | 8 ++++---- osu.Game/Overlays/Music/PlaylistOverlay.cs | 6 +++--- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index 1a3e38ddd7..09e5bc849e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -19,11 +19,11 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene { - private readonly BindableList> beatmapSets = new BindableList>(); + private readonly BindableList> beatmapSets = new BindableList>(); private PlaylistOverlay playlistOverlay; - private ILive first; + private Live first; [SetUp] public void Setup() => Schedule(() => @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("hold 1st item handle", () => { - var handle = this.ChildrenOfType>.PlaylistItemHandle>().First(); + var handle = this.ChildrenOfType>.PlaylistItemHandle>().First(); InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre); InputManager.PressButton(MouseButton.Left); }); diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs index c86146ff25..24d867141c 100644 --- a/osu.Game/Overlays/Music/Playlist.cs +++ b/osu.Game/Overlays/Music/Playlist.cs @@ -13,11 +13,11 @@ using osuTK; namespace osu.Game.Overlays.Music { - public class Playlist : OsuRearrangeableListContainer> + public class Playlist : OsuRearrangeableListContainer> { - public Action> RequestSelection; + public Action> RequestSelection; - public readonly Bindable> SelectedSet = new Bindable>(); + public readonly Bindable> SelectedSet = new Bindable>(); public new MarginPadding Padding { @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Music public void Filter(FilterCriteria criteria) { - var items = (SearchContainer>>)ListContainer; + var items = (SearchContainer>>)ListContainer; foreach (var item in items.OfType()) item.InSelectedCollection = criteria.Collection?.Beatmaps.Any(b => item.Model.ID == b.BeatmapSet?.ID) ?? true; @@ -35,15 +35,15 @@ namespace osu.Game.Overlays.Music items.SearchTerm = criteria.SearchText; } - public ILive FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); + public Live FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); - protected override OsuRearrangeableListItem> CreateOsuDrawable(ILive item) => new PlaylistItem(item) + protected override OsuRearrangeableListItem> CreateOsuDrawable(Live item) => new PlaylistItem(item) { SelectedSet = { BindTarget = SelectedSet }, RequestSelection = set => RequestSelection?.Invoke(set) }; - protected override FillFlowContainer>> CreateListFillFlowContainer() => new SearchContainer>> + protected override FillFlowContainer>> CreateListFillFlowContainer() => new SearchContainer>> { Spacing = new Vector2(0, 3), LayoutDuration = 200, diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 3f82580bfb..f081cc0503 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -17,11 +17,11 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Music { - public class PlaylistItem : OsuRearrangeableListItem>, IFilterable + public class PlaylistItem : OsuRearrangeableListItem>, IFilterable { - public readonly Bindable> SelectedSet = new Bindable>(); + public readonly Bindable> SelectedSet = new Bindable>(); - public Action> RequestSelection; + public Action> RequestSelection; private TextFlowContainer text; private ITextPart titlePart; @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Music [Resolved] private OsuColour colours { get; set; } - public PlaylistItem(ILive item) + public PlaylistItem(Live item) : base(item) { Padding = new MarginPadding { Left = 5 }; diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 4b10bb779e..7b705a905b 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -24,9 +24,9 @@ namespace osu.Game.Overlays.Music private const float transition_duration = 600; private const float playlist_height = 510; - public IBindableList> BeatmapSets => beatmapSets; + public IBindableList> BeatmapSets => beatmapSets; - private readonly BindableList> beatmapSets = new BindableList>(); + private readonly BindableList> beatmapSets = new BindableList>(); private readonly Bindable beatmap = new Bindable(); @@ -144,7 +144,7 @@ namespace osu.Game.Overlays.Music this.FadeOut(transition_duration); } - private void itemSelected(ILive beatmapSet) + private void itemSelected(Live beatmapSet) { beatmapSet.PerformRead(set => { From d0a2818847d85fe68e99a3cc5feb11ca4602e130 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 15:14:43 +0900 Subject: [PATCH 158/587] Fix incorrect testing --- osu.Game.Tests/NonVisual/ScoreInfoTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/NonVisual/ScoreInfoTest.cs b/osu.Game.Tests/NonVisual/ScoreInfoTest.cs index 836c978ba2..41b08a9e98 100644 --- a/osu.Game.Tests/NonVisual/ScoreInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ScoreInfoTest.cs @@ -34,8 +34,8 @@ namespace osu.Game.Tests.NonVisual Assert.That(scoreCopy.Rank, Is.EqualTo(ScoreRank.B)); Assert.That(score.Rank, Is.EqualTo(ScoreRank.X)); - Assert.That(scoreCopy.RealmUser.Username, Is.EqualTo("test")); - Assert.That(score.Rank, Is.Empty); + Assert.That(scoreCopy.RealmUser.Username, Is.Empty); + Assert.That(score.RealmUser.Username, Is.EqualTo("test")); } [Test] From 267a7bd21fa709fc941d3620fe2c57c48e5bbe4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 15:14:49 +0900 Subject: [PATCH 159/587] Give `RealmUser.Username` a better default value --- osu.Game/Models/RealmUser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Models/RealmUser.cs b/osu.Game/Models/RealmUser.cs index ff35528827..5fccff597c 100644 --- a/osu.Game/Models/RealmUser.cs +++ b/osu.Game/Models/RealmUser.cs @@ -11,7 +11,7 @@ namespace osu.Game.Models { public int OnlineID { get; set; } = 1; - public string Username { get; set; } + public string Username { get; set; } = string.Empty; public bool IsBot => false; From 4fe3d83fc40a176edf7ba9a1e3785c0fb57d24f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 15:21:14 +0900 Subject: [PATCH 160/587] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 4e5b9fdbb1..f85a96f819 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index af5d8a5920..aa6fb93aa0 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 2bcdea61b3..fbb4688588 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From 5ea781faef9a4fe2de3be7a72ce84ad2306ecf23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 15:24:53 +0900 Subject: [PATCH 161/587] `Send` unsubscribe actions to synchronization context for consistency and safety --- osu.Game/Database/RealmAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 3866138d40..3572446aef 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -300,7 +300,7 @@ namespace osu.Game.Database return new InvokeOnDisposal(() => { if (ThreadSafety.IsUpdateThread) - unsubscribe(); + syncContext.Send(_ => unsubscribe(), null); else syncContext.Post(_ => unsubscribe(), null); From b5e6352137d4254323d2888f308c81dc2ced3208 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 09:31:29 +0300 Subject: [PATCH 162/587] Revert `SettingsItem`-related changes --- osu.Game/Overlays/Settings/ISettingsItem.cs | 14 +--- osu.Game/Overlays/Settings/SettingsItem.cs | 91 +++++---------------- 2 files changed, 21 insertions(+), 84 deletions(-) diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index 20e2f48f96..e7afa48502 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -2,22 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Graphics; namespace osu.Game.Overlays.Settings { - /// - /// A non-generic interface for s. - /// - public interface ISettingsItem : IExpandable, IDisposable + public interface ISettingsItem : IDrawable, IDisposable { - /// - /// Invoked when the setting value has changed. - /// event Action SettingChanged; - - /// - /// Returns whether the UI control is currently in a dragged state. - /// - bool IsControlDragged { get; } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 29980dc5a8..e709be1343 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Settings protected readonly FillFlowContainer FlowContent; - private SpriteText label; + private SpriteText labelText; private OsuTextFlowContainer warningText; @@ -42,34 +42,21 @@ namespace osu.Game.Overlays.Settings [Resolved] private OsuColour colours { get; set; } - private LocalisableString labelText; - public virtual LocalisableString LabelText { - get => labelText; + get => labelText?.Text ?? string.Empty; set { - ensureLabelCreated(); + if (labelText == null) + { + // construct lazily for cases where the label is not needed (may be provided by the Control). + FlowContent.Insert(-1, labelText = new OsuSpriteText()); - labelText = value; - updateLabelText(); - } - } + updateDisabled(); + } - private LocalisableString? contractedLabelText; - - /// - /// Text to be displayed in place of when this is in a contracted state. - /// - public LocalisableString? ContractedLabelText - { - get => contractedLabelText; - set - { - ensureLabelCreated(); - - contractedLabelText = value; - updateLabelText(); + labelText.Text = value; + updateLayout(); } } @@ -103,12 +90,6 @@ namespace osu.Game.Overlays.Settings set => controlWithCurrent.Current = value; } - public BindableBool Expanded { get; } = new BindableBool(true); - - public bool IsControlDragged => Control.IsDragged; - - public event Action SettingChanged; - public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List(Keywords) { LabelText.ToString() }.ToArray(); public IEnumerable Keywords { get; set; } @@ -120,6 +101,8 @@ namespace osu.Game.Overlays.Settings public bool FilteringActive { get; set; } + public event Action SettingChanged; + protected SettingsItem() { RelativeSizeAxes = Axes.X; @@ -168,59 +151,23 @@ namespace osu.Game.Overlays.Settings Anchor = Anchor.Centre, Origin = Anchor.Centre }); + updateLayout(); } } - [Resolved(canBeNull: true)] - private IExpandingContainer expandingContainer { get; set; } - - protected override void LoadComplete() + private void updateLayout() { - base.LoadComplete(); + bool hasLabel = labelText != null && !string.IsNullOrEmpty(labelText.Text.ToString()); - expandingContainer?.Expanded.BindValueChanged(containerExpanded => Expanded.Value = containerExpanded.NewValue, true); - - Expanded.BindValueChanged(v => - { - updateLabelText(); - - Control.FadeTo(v.NewValue ? 1 : 0, 500, Easing.OutQuint); - Control.BypassAutoSizeAxes = v.NewValue ? Axes.None : Axes.Both; - }, true); - - FinishTransforms(true); - } - - private void ensureLabelCreated() - { - if (label != null) - return; - - // construct lazily for cases where the label is not needed (may be provided by the Control). - FlowContent.Insert(-1, label = new OsuSpriteText()); - - updateDisabled(); - } - - private void updateLabelText() - { - if (label != null) - { - if (contractedLabelText is LocalisableString contractedText) - label.Text = Expanded.Value ? labelText : contractedText; - else - label.Text = labelText; - } - - // if the settings item is providing a non-empty label, the default value indicator should be centred vertically to the left of the label. + // if the settings item is providing a label, the default value indicator should be centred vertically to the left of the label. // otherwise, it should be centred vertically to the left of the main control of the settings item. - defaultValueIndicatorContainer.Height = !string.IsNullOrEmpty(label?.Text.ToString()) ? label.DrawHeight : Control.DrawHeight; + defaultValueIndicatorContainer.Height = hasLabel ? labelText.DrawHeight : Control.DrawHeight; } private void updateDisabled() { - if (label != null) - label.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; + if (labelText != null) + labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } } } From 24bcba64183a7bf404eca503a3c31a24762bfdac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 15:57:05 +0900 Subject: [PATCH 163/587] Move final result set firing to before the update realm is disposed Without this, if any registered callback attempts to access `RealmAccess.Realm` when handling the empty set callback, it will deadlock the game. --- osu.Game/Database/RealmAccess.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 3572446aef..1a40176c1d 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -604,20 +604,18 @@ namespace osu.Game.Database Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); + // Force a flush of any pending callbacks in the synchronization context. + // We want to ensure that the empty set callbacks are the last thing to arrive. + syncContext?.Send(_ => + { + foreach (var action in notificationsResetMap.Values) + action(); + }, null); + updateRealm?.Dispose(); updateRealm = null; } - // In order to ensure events arrive in the correct order, these *must* be fired post disposal of the update realm, - // and must be posted to the synchronization context. - // This is because realm may fire event callbacks between the `unregisterAllSubscriptions` and `updateRealm.Dispose` - // calls above. - syncContext?.Send(_ => - { - foreach (var action in notificationsResetMap.Values) - action(); - }, null); - const int sleep_length = 200; int timeout = 5000; From 699826677006b6e7a230f061c505c53b2fbd73c6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 10:18:06 +0300 Subject: [PATCH 164/587] Add simplified implementation of an expandable slider --- osu.Game/Overlays/ExpandableSlider.cs | 123 ++++++++++++++++++++++++ osu.Game/Overlays/IExpandableControl.cs | 16 +++ 2 files changed, 139 insertions(+) create mode 100644 osu.Game/Overlays/ExpandableSlider.cs create mode 100644 osu.Game/Overlays/IExpandableControl.cs diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs new file mode 100644 index 0000000000..38faf44148 --- /dev/null +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -0,0 +1,123 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays +{ + /// + /// An implementation for the UI slider bar control. + /// + public class ExpandableSlider : CompositeDrawable, IExpandableControl, IHasCurrentValue + where T : struct, IEquatable, IComparable, IConvertible + where TSlider : OsuSliderBar, new() + { + private readonly OsuSpriteText label; + private readonly TSlider slider; + + private LocalisableString contractedLabelText; + + /// + /// The label text to display when this slider is in a contracted state. + /// + public LocalisableString ContractedLabelText + { + get => contractedLabelText; + set + { + if (value == contractedLabelText) + return; + + contractedLabelText = value; + + if (!Expanded.Value) + label.Text = value; + } + } + + private LocalisableString expandedLabelText; + + /// + /// The label text to display when this slider is in an expanded state. + /// + public LocalisableString ExpandedLabelText + { + get => expandedLabelText; + set + { + if (value == expandedLabelText) + return; + + expandedLabelText = value; + + if (Expanded.Value) + label.Text = value; + } + } + + public Bindable Current + { + get => slider.Current; + set => slider.Current = value; + } + + public BindableBool Expanded { get; } = new BindableBool(); + + public bool IsControlDragged => slider.IsDragged; + + public override bool HandlePositionalInput => true; + + public ExpandableSlider() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + label = new OsuSpriteText(), + slider = new TSlider(), + } + }; + } + + [Resolved(canBeNull: true)] + private IExpandingContainer expandingContainer { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + expandingContainer?.Expanded.BindValueChanged(containerExpanded => + { + Expanded.Value = containerExpanded.NewValue; + }, true); + + Expanded.BindValueChanged(v => + { + label.Text = v.NewValue ? expandedLabelText : contractedLabelText; + slider.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + slider.BypassAutoSizeAxes = v.NewValue ? Axes.Y : Axes.None; + }, true); + } + } + + /// + /// An implementation for the UI slider bar control. + /// + public class ExpandableSlider : ExpandableSlider> + where T : struct, IEquatable, IComparable, IConvertible + { + } +} diff --git a/osu.Game/Overlays/IExpandableControl.cs b/osu.Game/Overlays/IExpandableControl.cs new file mode 100644 index 0000000000..fae07ae23b --- /dev/null +++ b/osu.Game/Overlays/IExpandableControl.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. + +namespace osu.Game.Overlays +{ + /// + /// An interface for UI controls with the ability to expand/contract. + /// + public interface IExpandableControl : IExpandable + { + /// + /// Returns whether the UI control is currently in a dragged state. + /// + bool IsControlDragged { get; } + } +} From eb83b7fe0a912130641da4e56a9808ed18ce099b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 10:18:17 +0300 Subject: [PATCH 165/587] Update existing implementation with changes --- .../TestSceneExpandingControlContainer.cs | 15 +++++++-------- osu.Game/Overlays/ExpandingControlContainer.cs | 3 +-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs index d75089ceac..48089566cc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections; using osuTK; using osuTK.Input; @@ -20,8 +19,8 @@ namespace osu.Game.Tests.Visual.UserInterface private TestExpandingContainer container; private SettingsToolboxGroup toolboxGroup; - private SettingsSlider slider1; - private SettingsSlider slider2; + private ExpandableSlider slider1; + private ExpandableSlider slider2; [SetUp] public void SetUp() => Schedule(() => @@ -37,7 +36,7 @@ namespace osu.Game.Tests.Visual.UserInterface Width = 1, Children = new Drawable[] { - slider1 = new SettingsSlider + slider1 = new ExpandableSlider { Current = new BindableFloat { @@ -47,7 +46,7 @@ namespace osu.Game.Tests.Visual.UserInterface Precision = 0.01f, }, }, - slider2 = new SettingsSlider + slider2 = new ExpandableSlider { Current = new BindableDouble { @@ -63,13 +62,13 @@ namespace osu.Game.Tests.Visual.UserInterface slider1.Current.BindValueChanged(v => { - slider1.LabelText = $"Slider One ({v.NewValue:0.##x})"; + slider1.ExpandedLabelText = $"Slider One ({v.NewValue:0.##x})"; slider1.ContractedLabelText = $"S. 1. ({v.NewValue:0.##x})"; }, true); slider2.Current.BindValueChanged(v => { - slider2.LabelText = $"Slider Two ({v.NewValue:N2})"; + slider2.ExpandedLabelText = $"Slider Two ({v.NewValue:N2})"; slider2.ContractedLabelText = $"S. 2. ({v.NewValue:N2})"; }, true); }); @@ -172,7 +171,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("container still expanded", () => container.Expanded.Value); } - private class TestExpandingContainer : ExpandingControlContainer + private class TestExpandingContainer : ExpandingControlContainer { public TestExpandingContainer() : base(120, 250) diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs index 2accd63fb9..fb6a71ba97 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -9,7 +9,6 @@ using osu.Framework.Input.Events; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Containers; -using osu.Game.Overlays.Settings; namespace osu.Game.Overlays { @@ -119,6 +118,6 @@ namespace osu.Game.Overlays /// /// Whether the given control is currently active, by checking whether it's hovered or dragged. /// - private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is ISettingsItem item && item.IsControlDragged); + private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is IExpandableControl expandable && expandable.IsControlDragged); } } From 11f0f3c17de6986c7aa6f6a3915b239bf2b5543e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 16:21:24 +0900 Subject: [PATCH 166/587] Revert "Move final result set firing to before the update realm is disposed" This reverts commit 24bcba64183a7bf404eca503a3c31a24762bfdac. --- osu.Game/Database/RealmAccess.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 1a40176c1d..3572446aef 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -604,18 +604,20 @@ namespace osu.Game.Database Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); - // Force a flush of any pending callbacks in the synchronization context. - // We want to ensure that the empty set callbacks are the last thing to arrive. - syncContext?.Send(_ => - { - foreach (var action in notificationsResetMap.Values) - action(); - }, null); - updateRealm?.Dispose(); updateRealm = null; } + // In order to ensure events arrive in the correct order, these *must* be fired post disposal of the update realm, + // and must be posted to the synchronization context. + // This is because realm may fire event callbacks between the `unregisterAllSubscriptions` and `updateRealm.Dispose` + // calls above. + syncContext?.Send(_ => + { + foreach (var action in notificationsResetMap.Values) + action(); + }, null); + const int sleep_length = 200; int timeout = 5000; From 161ff45f8cb5e93d7cf2a02c5601344d9d35064c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 10:35:42 +0300 Subject: [PATCH 167/587] Resolve further UI-related issues --- osu.Game/Overlays/ExpandableSlider.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs index 38faf44148..524485d806 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Overlays { @@ -84,10 +85,14 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), Children = new Drawable[] { label = new OsuSpriteText(), - slider = new TSlider(), + slider = new TSlider + { + RelativeSizeAxes = Axes.X, + }, } }; } @@ -108,7 +113,7 @@ namespace osu.Game.Overlays { label.Text = v.NewValue ? expandedLabelText : contractedLabelText; slider.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); - slider.BypassAutoSizeAxes = v.NewValue ? Axes.Y : Axes.None; + slider.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; }, true); } } From 791ea0308f8026c127d26ee2118b083ffa72abf9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 17:09:28 +0900 Subject: [PATCH 168/587] Add flag to guard against deadlocks during blocking operations --- osu.Game/Database/RealmAccess.cs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 3572446aef..dc7c8bf668 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -89,10 +89,15 @@ namespace osu.Game.Database private Realm? updateRealm; + private bool isSendingNotificationResetEvents; + public Realm Realm => ensureUpdateRealm(); private Realm ensureUpdateRealm() { + if (isSendingNotificationResetEvents) + throw new InvalidOperationException("Cannot retrieve a realm context from a notification callback during a blocking operation."); + if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException(@$"Use {nameof(getRealmInstance)} when performing realm operations from a non-update thread"); @@ -614,8 +619,20 @@ namespace osu.Game.Database // calls above. syncContext?.Send(_ => { - foreach (var action in notificationsResetMap.Values) - action(); + // Flag ensures that we don't get in a deadlocked scenario due to a callback attempting to access `RealmAccess.Realm` or `RealmAccess.Run` + // and hitting `realmRetrievalLock` a second time. Generally such usages should not exist, and as such we throw when an attempt is made + // to use in this fashion. + isSendingNotificationResetEvents = true; + + try + { + foreach (var action in notificationsResetMap.Values) + action(); + } + finally + { + isSendingNotificationResetEvents = false; + } }, null); const int sleep_length = 200; From 885fb92aada99e4dfccb0904f544e16a093787ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 17:21:57 +0900 Subject: [PATCH 169/587] Move final empty result set sending to post-compact --- osu.Game/Database/RealmAccess.cs | 44 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index dc7c8bf668..9bdbebfe89 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -613,28 +613,6 @@ namespace osu.Game.Database updateRealm = null; } - // In order to ensure events arrive in the correct order, these *must* be fired post disposal of the update realm, - // and must be posted to the synchronization context. - // This is because realm may fire event callbacks between the `unregisterAllSubscriptions` and `updateRealm.Dispose` - // calls above. - syncContext?.Send(_ => - { - // Flag ensures that we don't get in a deadlocked scenario due to a callback attempting to access `RealmAccess.Realm` or `RealmAccess.Run` - // and hitting `realmRetrievalLock` a second time. Generally such usages should not exist, and as such we throw when an attempt is made - // to use in this fashion. - isSendingNotificationResetEvents = true; - - try - { - foreach (var action in notificationsResetMap.Values) - action(); - } - finally - { - isSendingNotificationResetEvents = false; - } - }, null); - const int sleep_length = 200; int timeout = 5000; @@ -656,6 +634,28 @@ namespace osu.Game.Database // We still want to continue with the blocking operation, though. Logger.Log($"Realm compact failed with error {e}", LoggingTarget.Database); } + + // In order to ensure events arrive in the correct order, these *must* be fired post disposal of the update realm, + // and must be posted to the synchronization context. + // This is because realm may fire event callbacks between the `unregisterAllSubscriptions` and `updateRealm.Dispose` + // calls above. + syncContext?.Send(_ => + { + // Flag ensures that we don't get in a deadlocked scenario due to a callback attempting to access `RealmAccess.Realm` or `RealmAccess.Run` + // and hitting `realmRetrievalLock` a second time. Generally such usages should not exist, and as such we throw when an attempt is made + // to use in this fashion. + isSendingNotificationResetEvents = true; + + try + { + foreach (var action in notificationsResetMap.Values) + action(); + } + finally + { + isSendingNotificationResetEvents = false; + } + }, null); } catch { From d1a22562621a2768056d3604400108f0b20c5441 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 17:48:11 +0900 Subject: [PATCH 170/587] Refactor `SkinSection` to avoid unnecessary realm queries --- .../Overlays/Settings/Sections/SkinSection.cs | 83 ++++++++++--------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 0846c023c1..441a439596 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Skinning; using osu.Game.Skinning.Editor; +using Realms; namespace osu.Game.Overlays.Settings.Sections { @@ -51,12 +52,6 @@ namespace osu.Game.Overlays.Settings.Sections private IDisposable realmSubscription; - private IQueryable queryRealmSkins() => - realm.Realm.All() - .Where(s => !s.DeletePending) - .OrderByDescending(s => s.Protected) // protected skins should be at the top. - .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase); - [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor) { @@ -83,37 +78,54 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Current = dropdownBindable; - realmSubscription = realm.RegisterForNotifications(r => queryRealmSkins(), (sender, changes, error) => - { - // The first fire of this is a bit redundant due to the call below, - // but this is safest in case the subscription is restored after a context recycle. - updateItems(); - }); - - updateItems(); + realmSubscription = realm.RegisterForNotifications(r => realm.Realm.All() + .Where(s => !s.DeletePending) + .OrderByDescending(s => s.Protected) // protected skins should be at the top. + .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase), skinsChanged); configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig)); - updateSelectedSkinFromConfig(); - dropdownBindable.BindValueChanged(skin => + dropdownBindable.BindValueChanged(dropdownSelectionChanged); + } + + private void dropdownSelectionChanged(ValueChangedEvent> skin) + { + // Only handle cases where it's clear the user has intent to change skins. + if (skin.OldValue == null) return; + + if (skin.NewValue.Equals(random_skin_info)) { - if (skin.NewValue.Equals(random_skin_info)) + var skinBefore = skins.CurrentSkinInfo.Value; + + skins.SelectRandomSkin(); + + if (skinBefore == skins.CurrentSkinInfo.Value) { - var skinBefore = skins.CurrentSkinInfo.Value; - - skins.SelectRandomSkin(); - - if (skinBefore == skins.CurrentSkinInfo.Value) - { - // the random selection didn't change the skin, so we should manually update the dropdown to match. - dropdownBindable.Value = skins.CurrentSkinInfo.Value; - } - - return; + // the random selection didn't change the skin, so we should manually update the dropdown to match. + dropdownBindable.Value = skins.CurrentSkinInfo.Value; } - configBindable.Value = skin.NewValue.ID.ToString(); - }); + return; + } + + configBindable.Value = skin.NewValue.ID.ToString(); + } + + private void skinsChanged(IRealmCollection skins, ChangeSet changes, Exception error) + { + // This can only mean that realm is recycling, else we would see the protected skins. + // Because we are using `Live<>` in this class, we don't need to worry about this scenario too much. + if (!skins.Any()) + return; + + int protectedCount = skins.Count(s => s.Protected); + + skinItems = skins.ToLive(realm); + skinItems.Insert(protectedCount, random_skin_info); + + skinDropdown.Items = skinItems; + + updateSelectedSkinFromConfig(); } private void updateSelectedSkinFromConfig() @@ -126,17 +138,6 @@ namespace osu.Game.Overlays.Settings.Sections dropdownBindable.Value = skin ?? skinDropdown.Items.First(); } - private void updateItems() - { - int protectedCount = queryRealmSkins().Count(s => s.Protected); - - skinItems = queryRealmSkins().ToLive(realm); - - skinItems.Insert(protectedCount, random_skin_info); - - skinDropdown.Items = skinItems; - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From f2d48d088d81773f1d93f298b8e7d142565b7e80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 17:57:03 +0900 Subject: [PATCH 171/587] Fix realm migration failures with presence of databased EF rulesets that don't exist on disk --- osu.Game/Rulesets/EFRulesetInfo.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs index 473b7c657e..9559cfa00c 100644 --- a/osu.Game/Rulesets/EFRulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -28,21 +28,15 @@ namespace osu.Game.Rulesets public Ruleset CreateInstance() { if (!Available) - throw new RulesetLoadException(@"Ruleset not available"); + return null; var type = Type.GetType(InstantiationInfo); if (type == null) - throw new RulesetLoadException(@"Type lookup failure"); + return null; var ruleset = Activator.CreateInstance(type) as Ruleset; - if (ruleset == null) - throw new RulesetLoadException(@"Instantiation failure"); - - // overwrite the pre-populated RulesetInfo with a potentially database attached copy. - // ruleset.RulesetInfo = this; - return ruleset; } From 3aa681005bd5bf676b0cdf912aa0567a8120f077 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 18:04:53 +0900 Subject: [PATCH 172/587] Skip importing scores which have no matching realm ruleset There's no real way to recover these unless we want to start importing rulesets into realm. And that seems counter productive. This can only happen if users don't have the dll present any more, and it was removed far before realm was tracking rulesets (else it would have an `Available=0` entry in realm to match). --- osu.Game/Database/EFToRealmMigrator.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index adf91e4a41..33f7c25b28 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -286,6 +286,7 @@ namespace osu.Game.Database var transaction = r.BeginWrite(); int written = 0; + int missing = 0; try { @@ -300,6 +301,13 @@ namespace osu.Game.Database var beatmap = r.All().First(b => b.Hash == score.BeatmapInfo.Hash); var ruleset = r.Find(score.Ruleset.ShortName); + + if (ruleset == null) + { + log($"Skipping {++missing} scores with missing ruleset"); + continue; + } + var user = new RealmUser { OnlineID = score.User.OnlineID, From 45636ce04b224ca516b798c253f2289c357aaa5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 18:25:27 +0900 Subject: [PATCH 173/587] Remove collection `ToLive` helper method to avoid confusion --- osu.Game/Database/RealmObjectExtensions.cs | 6 ------ .../Overlays/Settings/Sections/SkinSection.cs | 18 +++++++++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index dba8633f53..7a0ca2c85a 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -216,12 +216,6 @@ namespace osu.Game.Database return new RealmLiveUnmanaged(realmObject); } - public static List> ToLive(this IEnumerable realmList, RealmAccess realm) - where T : RealmObject, IHasGuidPrimaryKey - { - return realmList.Select(l => new RealmLive(l, realm)).Cast>().ToList(); - } - public static Live ToLive(this T realmObject, RealmAccess realm) where T : RealmObject, IHasGuidPrimaryKey { diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 441a439596..1dfe49945f 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Settings.Sections Name = "", }.ToLiveUnmanaged(); - private List> skinItems; + private readonly List> dropdownItems = new List>(); [Resolved] private SkinManager skins { get; set; } @@ -111,19 +111,23 @@ namespace osu.Game.Overlays.Settings.Sections configBindable.Value = skin.NewValue.ID.ToString(); } - private void skinsChanged(IRealmCollection skins, ChangeSet changes, Exception error) + private void skinsChanged(IRealmCollection sender, ChangeSet changes, Exception error) { // This can only mean that realm is recycling, else we would see the protected skins. // Because we are using `Live<>` in this class, we don't need to worry about this scenario too much. - if (!skins.Any()) + if (!sender.Any()) return; - int protectedCount = skins.Count(s => s.Protected); + int protectedCount = sender.Count(s => s.Protected); - skinItems = skins.ToLive(realm); - skinItems.Insert(protectedCount, random_skin_info); + // For simplicity repopulate the full list. + // In the future we should change this to properly handle ChangeSet events. + dropdownItems.Clear(); + foreach (var skin in sender) + dropdownItems.Add(skin.ToLive(realm)); + dropdownItems.Insert(protectedCount, random_skin_info); - skinDropdown.Items = skinItems; + skinDropdown.Items = dropdownItems; updateSelectedSkinFromConfig(); } From 473c4d00cabb5079b8c1630dd46659889cb25665 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 18:38:44 +0900 Subject: [PATCH 174/587] Fix grouped difficulty icons using incorrect lookup for ruleset grouping --- osu.Game/Screens/Select/Carousel/SetPanelContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 619806f96e..82523c9d9d 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Select.Carousel var beatmaps = carouselSet.Beatmaps.ToList(); return beatmaps.Count > maximum_difficulty_icons - ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.RulesetID) + ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset.ShortName) .Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset)) : beatmaps.Select(b => new FilterableDifficultyIcon(b)); } From 57e624d8e7519427fbe05a54da4fe7bed5f7cf67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 18:46:32 +0900 Subject: [PATCH 175/587] Fix custom rulesets being displayed before official ones --- osu.Game/Rulesets/RulesetStore.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 9af9ace7ad..0f518be639 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -163,7 +163,10 @@ namespace osu.Game.Rulesets } } - availableRulesets.AddRange(detachedRulesets); + // add known official rulesets first.. + availableRulesets.AddRange(detachedRulesets.Where(r => r.OnlineID >= 0).OrderBy(r => r.OnlineID)); + // .. then add any customs + availableRulesets.AddRange(detachedRulesets.Where(r => r.OnlineID < 0).OrderBy(r => r.ShortName)); }); } From abe2cccaaecd1a01b5f3855705b4195958ef11da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 19:03:26 +0900 Subject: [PATCH 176/587] Fix completely invalid method of testing realm migration --- osu.Game.Tests/Database/RealmLiveTests.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 3f81b36378..4bc1f5078a 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -47,16 +47,14 @@ namespace osu.Game.Tests.Database liveBeatmap = beatmap.ToLive(realm); }); - using (realm.BlockAllOperations()) - { - // recycle realm before migrating - } - using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target")) { migratedStorage.DeleteDirectory(string.Empty); - storage.Migrate(migratedStorage); + using (realm.BlockAllOperations()) + { + storage.Migrate(migratedStorage); + } Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden)); } From b3f2392358ac141b17f28328932d299384e3bec9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 19:04:18 +0900 Subject: [PATCH 177/587] Resolve compilation error due to removed method --- osu.Game/Overlays/Music/PlaylistOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 7b705a905b..59ade0918d 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Music { beatmapSets.Clear(); // must use AddRange to avoid RearrangeableList sort overhead per add op. - beatmapSets.AddRange(sender.ToLive(realm)); + beatmapSets.AddRange(sender.Select(b => b.ToLive(realm))); return; } From 378173cc66e202e97a342b51c6f0f4a6327cf278 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jan 2022 22:46:29 +0900 Subject: [PATCH 178/587] Fix some score imports failing due to null string attempted to be parsed as json --- osu.Game/Scoring/EFScoreInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/EFScoreInfo.cs b/osu.Game/Scoring/EFScoreInfo.cs index 1dd4e3b6b3..4161336cfc 100644 --- a/osu.Game/Scoring/EFScoreInfo.cs +++ b/osu.Game/Scoring/EFScoreInfo.cs @@ -105,7 +105,7 @@ namespace osu.Game.Scoring public string ModsJson { get => JsonConvert.SerializeObject(APIMods); - set => APIMods = JsonConvert.DeserializeObject(value); + set => APIMods = !string.IsNullOrEmpty(value) ? JsonConvert.DeserializeObject(value) : Array.Empty(); } [NotMapped] From 7f34085baa52a837edef6e1e1ed431f186d08224 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 17:04:28 +0300 Subject: [PATCH 179/587] Mark `EditorSavingTestScene` as abstract --- osu.Game/Tests/Visual/EditorSavingTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/EditorSavingTestScene.cs b/osu.Game/Tests/Visual/EditorSavingTestScene.cs index 72b5d076a5..cc39ead1de 100644 --- a/osu.Game/Tests/Visual/EditorSavingTestScene.cs +++ b/osu.Game/Tests/Visual/EditorSavingTestScene.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual /// /// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select. /// - public class EditorSavingTestScene : OsuGameTestScene + public abstract class EditorSavingTestScene : OsuGameTestScene { protected Editor Editor => Game.ChildrenOfType().FirstOrDefault(); From 873d3676153521b732167bfeb052445931984846 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Wed, 26 Jan 2022 15:51:39 +0100 Subject: [PATCH 180/587] Fix custom rulesets not being able to convert maps --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 6b198ab505..7fd312371b 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select.Carousel bool match = criteria.Ruleset == null || BeatmapInfo.RulesetID == criteria.Ruleset.OnlineID || - (BeatmapInfo.RulesetID == 0 && criteria.Ruleset.OnlineID > 0 && criteria.AllowConvertedBeatmaps); + (BeatmapInfo.RulesetID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps); if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { From e712fab2994557019a825c80d8658197fca51b21 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Wed, 26 Jan 2022 16:14:07 +0100 Subject: [PATCH 181/587] Add test for custom ruleset conversion filtering --- .../NonVisual/Filtering/FilterMatchingTest.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 74904f4585..2e7a4a65e5 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -78,6 +78,20 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.IsFalse(carouselItem.Filtered.Value); } + [Test] + public void TestCriteriaMatchingConvertedBeatmapsForCustomRulesets() + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Ruleset = new RulesetInfo { OnlineID = -25 }, + AllowConvertedBeatmaps = true + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.IsFalse(carouselItem.Filtered.Value); + } + [Test] [TestCase(true)] [TestCase(false)] From 6ec9c5c21a7891c0d1d7e8f9e88b44f482885da1 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Wed, 26 Jan 2022 16:23:00 +0100 Subject: [PATCH 182/587] Use default custom ruleset ID --- osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 2e7a4a65e5..363753cb33 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.NonVisual.Filtering var exampleBeatmapInfo = getExampleBeatmap(); var criteria = new FilterCriteria { - Ruleset = new RulesetInfo { OnlineID = -25 }, + Ruleset = new RulesetInfo { OnlineID = -1 }, AllowConvertedBeatmaps = true }; var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); From f21e3d0d866e637870d9a9693b693b108c114611 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 00:34:51 +0900 Subject: [PATCH 183/587] Block collection loading until realm migration has completed --- osu.Game/Collections/CollectionManager.cs | 5 +++++ osu.Game/Database/DatabaseContextFactory.cs | 6 ++++++ osu.Game/Database/EFToRealmMigrator.cs | 1 + 3 files changed, 12 insertions(+) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index c4f991094c..5845e0d4d1 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -50,9 +50,14 @@ namespace osu.Game.Collections this.storage = storage; } + [Resolved(canBeNull: true)] + private DatabaseContextFactory efContextFactory { get; set; } = null!; + [BackgroundDependencyLoader] private void load() { + efContextFactory?.WaitForMigrationCompletion(); + Collections.CollectionChanged += collectionsChanged; if (storage.Exists(database_backup_name)) diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index c84edbfb81..123ed8019c 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -184,5 +184,11 @@ namespace osu.Game.Database } public static string CreateDatabaseConnectionString(string filename, Storage storage) => string.Concat("Data Source=", storage.GetFullPath($@"{filename}", true)); + + private readonly ManualResetEventSlim migrationComplete = new ManualResetEventSlim(); + + public void SetMigrationCompletion() => migrationComplete.Set(); + + public void WaitForMigrationCompletion() => migrationComplete.Wait(); } } diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 33f7c25b28..96baad35ec 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -128,6 +128,7 @@ namespace osu.Game.Database }, TaskCreationOptions.LongRunning).ContinueWith(t => { migrationCompleted.SetResult(true); + efContextFactory.SetMigrationCompletion(); }); } From 0c2ed2f9a77c863865844f20bc0796ef1d6e14e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 01:25:55 +0900 Subject: [PATCH 184/587] Add failing test coverage of incorrect filter ruleset matching --- osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 363753cb33..33204d33a7 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -16,7 +16,11 @@ namespace osu.Game.Tests.NonVisual.Filtering { private BeatmapInfo getExampleBeatmap() => new BeatmapInfo { - Ruleset = new RulesetInfo { OnlineID = 0 }, + Ruleset = new RulesetInfo + { + ShortName = "osu", + OnlineID = 0 + }, StarRating = 4.0d, Difficulty = new BeatmapDifficulty { @@ -57,7 +61,7 @@ namespace osu.Game.Tests.NonVisual.Filtering var exampleBeatmapInfo = getExampleBeatmap(); var criteria = new FilterCriteria { - Ruleset = new RulesetInfo { OnlineID = 6 } + Ruleset = new RulesetInfo { ShortName = "catch" } }; var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); carouselItem.Filter(criteria); From f70e10e8a4607dd51254a9915c78880b35a4f923 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 01:25:40 +0900 Subject: [PATCH 185/587] Fix ruleset filter matching using `OnlineID` instead of `ShortName` --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 7fd312371b..e6dee0233f 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Select.Carousel bool match = criteria.Ruleset == null || - BeatmapInfo.RulesetID == criteria.Ruleset.OnlineID || + BeatmapInfo.Ruleset.ShortName == criteria.Ruleset.ShortName || (BeatmapInfo.RulesetID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps); if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) From 4382adad8240fee9c85f1b5cb21efea70c011184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jan 2022 23:20:33 +0100 Subject: [PATCH 186/587] Add test coverage for editor changes not resetting after exit without save --- .../Visual/Editing/TestSceneEditorSaving.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 58daab1ce2..adaa24d542 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -4,11 +4,13 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; +using osu.Game.Screens.Select; using osuTK.Input; namespace osu.Game.Tests.Visual.Editing @@ -116,5 +118,24 @@ namespace osu.Game.Tests.Visual.Editing EditorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT && EditorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT); } + + [Test] + public void TestExitWithoutSaveFromExistingBeatmap() + { + const string tags_to_save = "these tags will be saved"; + const string tags_to_discard = "these tags should be discarded"; + + AddStep("Set tags", () => EditorBeatmap.BeatmapInfo.Metadata.Tags = tags_to_save); + SaveEditor(); + AddAssert("Tags saved correctly", () => EditorBeatmap.BeatmapInfo.Metadata.Tags == tags_to_save); + + ReloadEditorToSameBeatmap(); + AddAssert("Tags saved correctly", () => EditorBeatmap.BeatmapInfo.Metadata.Tags == tags_to_save); + AddStep("Set tags again", () => EditorBeatmap.BeatmapInfo.Metadata.Tags = tags_to_discard); + + AddStep("Exit editor", () => Editor.Exit()); + AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + AddAssert("Tags reverted correctly", () => Game.Beatmap.Value.BeatmapInfo.Metadata.Tags == tags_to_save); + } } } From d760283665b6ecadcdee4989ded6e7684387dd41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jan 2022 23:31:42 +0100 Subject: [PATCH 187/587] Ensure edited beatmap is restored to a baseline state on exit --- osu.Game/Screens/Edit/Editor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b42f629aad..e26453f99a 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -574,7 +574,9 @@ namespace osu.Game.Screens.Edit // To update the game-wide beatmap with any changes, perform a re-fetch on exit/suspend. // This is required as the editor makes its local changes via EditorBeatmap // (which are not propagated outwards to a potentially cached WorkingBeatmap). - var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo); + ((IWorkingBeatmapCache)beatmapManager).Invalidate(Beatmap.Value.BeatmapInfo); + var refetchedBeatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == Beatmap.Value.BeatmapInfo.ID); + var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(refetchedBeatmapInfo); if (!(refetchedBeatmap is DummyWorkingBeatmap)) { From e0616476e2a0a772c5998b0194d8a0dab0ad0eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Jan 2022 20:52:14 +0100 Subject: [PATCH 188/587] Fix test gameplay tests failing due to beatmap refetch on suspend --- .../Visual/Editing/TestSceneEditorTestGameplay.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 79afc8cf27..e2406a8d27 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -70,7 +70,11 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); AddUntilStep("background has correct params", () => { - var background = this.ChildrenOfType().Single(); + // the test gameplay player's beatmap may be the "same" beatmap as the one being edited, *but* the `BeatmapInfo` references may differ + // due to the beatmap refetch logic ran on editor suspend. + // this test cares about checking the background belonging to the editor specifically, so check that using reference equality + // (as `.Equals()` cannot discern between the two, as they technically share the same database GUID). + var background = this.ChildrenOfType().Single(b => ReferenceEquals(b.Beatmap.BeatmapInfo, EditorBeatmap.BeatmapInfo)); return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0; }); AddAssert("no mods selected", () => SelectedMods.Value.Count == 0); @@ -99,7 +103,11 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); AddUntilStep("background has correct params", () => { - var background = this.ChildrenOfType().Single(); + // the test gameplay player's beatmap may be the "same" beatmap as the one being edited, *but* the `BeatmapInfo` references may differ + // due to the beatmap refetch logic ran on editor suspend. + // this test cares about checking the background belonging to the editor specifically, so check that using reference equality + // (as `.Equals()` cannot discern between the two, as they technically share the same database GUID). + var background = this.ChildrenOfType().Single(b => ReferenceEquals(b.Beatmap.BeatmapInfo, EditorBeatmap.BeatmapInfo)); return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0; }); From 587c0f965c877ee05db77ad87762ee9c810f4366 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 14:32:20 +0900 Subject: [PATCH 189/587] Add more attempts to delete EF database Just noticed in passing. Probably best we do this since it was known to fail on windows in some rare cases. --- osu.Game/Database/DatabaseContextFactory.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index 123ed8019c..45557aa5ec 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.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.IO; using System.Linq; using System.Threading; @@ -163,7 +164,24 @@ namespace osu.Game.Database try { - storage.Delete(DATABASE_NAME); + int attempts = 10; + + // Retry logic taken from MigratableStorage.AttemptOperation. + while (true) + { + try + { + storage.Delete(DATABASE_NAME); + return; + } + catch (Exception) + { + if (attempts-- == 0) + throw; + } + + Thread.Sleep(250); + } } catch { From deb5d75b5f21078d0877fd1ce5b75468fc896e3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 14:33:44 +0900 Subject: [PATCH 190/587] Change migration process to always delete old EF database It is already backed up, so this is probably fine. --- osu.Game/Database/EFToRealmMigrator.cs | 44 +++++++++++++++++++++----- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 96baad35ec..072d1c95f1 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -15,6 +15,8 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Models; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Skinning; @@ -40,6 +42,12 @@ namespace osu.Game.Database [Resolved] private OsuConfigManager config { get; set; } = null!; + [Resolved] + private NotificationOverlay notificationOverlay { get; set; } = null!; + + [Resolved] + private OsuGame game { get; set; } = null!; + private readonly OsuSpriteText currentOperationText; public EFToRealmMigrator() @@ -96,7 +104,11 @@ namespace osu.Game.Database protected override void LoadComplete() { base.LoadComplete(); + beginMigration(); + } + private void beginMigration() + { Task.Factory.StartNew(() => { using (var ef = efContextFactory.Get()) @@ -117,21 +129,37 @@ namespace osu.Game.Database migrateBeatmaps(ef); migrateScores(ef); } - - // Delete the database permanently. - // Will cause future startups to not attempt migration. - log("Migration successful, deleting EF database"); - efContextFactory.ResetDatabase(); - - if (DebugUtils.IsDebugBuild) - Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important); }, TaskCreationOptions.LongRunning).ContinueWith(t => { + if (t.Exception == null) + { + log("Migration successful!"); + + if (DebugUtils.IsDebugBuild) + Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important); + } + else + { + log("Migration failed!"); + Logger.Log(t.Exception.ToString(), LoggingTarget.Database); + } + + // Regardless of success, since the game is going to continue with startup let's move the ef database out of the way. + // If we were to not do this, the migration would run another time the next time the user starts the game. + deletePreRealmData(); + migrationCompleted.SetResult(true); efContextFactory.SetMigrationCompletion(); }); } + private void deletePreRealmData() + { + // Delete the database permanently. + // Will cause future startups to not attempt migration. + efContextFactory.ResetDatabase(); + } + private void log(string message) { Logger.Log(message, LoggingTarget.Database); From b745252962b232e3bc3173491ab37495f302a56d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 14:34:18 +0900 Subject: [PATCH 191/587] Show notification when migration fails to give users a recovery path --- osu.Game/Database/EFToRealmMigrator.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 072d1c95f1..d110ac2f9f 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -142,6 +142,16 @@ namespace osu.Game.Database { log("Migration failed!"); Logger.Log(t.Exception.ToString(), LoggingTarget.Database); + + notificationOverlay.Post(new SimpleErrorNotification + { + Text = "IMPORTANT: During data migration, some of your data could not be successfully migrated. It has been backed up in your osu! folder.\n\nFor further assistance, please open a discussion on github and attach your backup files (click to get started).", + Activated = () => + { + game.OpenUrlExternally(@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue&body=Please%20attach%20your%20database%20backups%20by%20zipping%20them%20and%20dragging%20in%20here!&category=q-a"); + return true; + } + }); } // Regardless of success, since the game is going to continue with startup let's move the ef database out of the way. From 08948f60f3108ccfaf098be3ebe99a69ffe61c93 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 14:39:07 +0900 Subject: [PATCH 192/587] Move backups to "backups" subfolder to make them easier to find --- osu.Game/OsuGameBase.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 09ca4e450d..8363c41437 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -202,16 +202,18 @@ namespace osu.Game // See https://github.com/ppy/osu/pull/16547 for more discussion. if (EFContextFactory != null) { + const string backup_folder = "backups"; + string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"; - EFContextFactory.CreateBackup($"client.{migration}.db"); - realm.CreateBackup($"client.{migration}.realm"); + EFContextFactory.CreateBackup(Path.Combine(backup_folder, $"client.{migration}.db")); + realm.CreateBackup(Path.Combine(backup_folder, $"client.{migration}.realm")); using (var source = Storage.GetStream("collection.db")) { if (source != null) { - using (var destination = Storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew)) + using (var destination = Storage.GetStream(Path.Combine(backup_folder, $"collection.{migration}.db"), FileAccess.Write, FileMode.CreateNew)) source.CopyTo(destination); } } From 31abb372e5c2ac123decc43f88aa8d68b5d3c2ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 14:52:43 +0900 Subject: [PATCH 193/587] Automatically zip and show the backup archive to the user --- osu.Game/Database/EFToRealmMigrator.cs | 29 ++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index d110ac2f9f..c41461cc68 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.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.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -9,6 +10,7 @@ using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; @@ -22,6 +24,10 @@ using osu.Game.Scoring; using osu.Game.Skinning; using osuTK; using Realms; +using SharpCompress.Archives; +using SharpCompress.Archives.Zip; +using SharpCompress.Common; +using SharpCompress.Writers.Zip; #nullable enable @@ -48,6 +54,9 @@ namespace osu.Game.Database [Resolved] private OsuGame game { get; set; } = null!; + [Resolved] + private Storage storage { get; set; } = null!; + private readonly OsuSpriteText currentOperationText; public EFToRealmMigrator() @@ -145,10 +154,26 @@ namespace osu.Game.Database notificationOverlay.Post(new SimpleErrorNotification { - Text = "IMPORTANT: During data migration, some of your data could not be successfully migrated. It has been backed up in your osu! folder.\n\nFor further assistance, please open a discussion on github and attach your backup files (click to get started).", + Text = "IMPORTANT: During data migration, some of your data could not be successfully migrated. The previous version has been backed up.\n\nFor further assistance, please open a discussion on github and attach your backup files (click to get started).", Activated = () => { - game.OpenUrlExternally(@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue&body=Please%20attach%20your%20database%20backups%20by%20zipping%20them%20and%20dragging%20in%20here!&category=q-a"); + game.OpenUrlExternally(@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue&body=Please%20drag%20the%20""attach_me.zip""%20file%20here!&category=q-a"); + + const string attachment_filename = "attach_me.zip"; + const string backup_folder = "backups"; + + var backupStorage = storage.GetStorageForDirectory(backup_folder); + + backupStorage.Delete(attachment_filename); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(backupStorage.GetFullPath(string.Empty)); + zip.SaveTo(Path.Combine(backupStorage.GetFullPath(string.Empty), attachment_filename), new ZipWriterOptions(CompressionType.Deflate)); + } + + backupStorage.PresentFileExternally(attachment_filename); + return true; } }); From 465e7d29feb9529dbde477bcf384bf8ef94b2949 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 14:53:11 +0900 Subject: [PATCH 194/587] Avoid showing the external link warning --- osu.Game/Database/EFToRealmMigrator.cs | 2 +- osu.Game/Online/Chat/ExternalLinkOpener.cs | 4 ++-- osu.Game/OsuGame.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index c41461cc68..05bc86a7c0 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -157,7 +157,7 @@ namespace osu.Game.Database Text = "IMPORTANT: During data migration, some of your data could not be successfully migrated. The previous version has been backed up.\n\nFor further assistance, please open a discussion on github and attach your backup files (click to get started).", Activated = () => { - game.OpenUrlExternally(@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue&body=Please%20drag%20the%20""attach_me.zip""%20file%20here!&category=q-a"); + game.OpenUrlExternally(@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue&body=Please%20drag%20the%20""attach_me.zip""%20file%20here!&category=q-a", true); const string attachment_filename = "attach_me.zip"; const string backup_folder = "backups"; diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 8407e2ca6a..328b43c4e8 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -27,9 +27,9 @@ namespace osu.Game.Online.Chat externalLinkWarning = config.GetBindable(OsuSetting.ExternalLinkWarning); } - public void OpenUrlExternally(string url) + public void OpenUrlExternally(string url, bool bypassWarning = false) { - if (externalLinkWarning.Value) + if (!bypassWarning && externalLinkWarning.Value) dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url))); else host.OpenUrlExternally(url); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c2e1b25d94..5b58dec0c3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -357,12 +357,12 @@ namespace osu.Game } }); - public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ => + public void OpenUrlExternally(string url, bool bypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ => { if (url.StartsWith('/')) url = $"{API.APIEndpointUrl}{url}"; - externalLinkOpener.OpenUrlExternally(url); + externalLinkOpener.OpenUrlExternally(url, bypassExternalUrlWarning); }); /// From fb081384e13ca4a09bee571636021ea0ce894dc2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 14:55:52 +0900 Subject: [PATCH 195/587] Add safety against zip creation potentially failing (probably can't but still) --- osu.Game/Database/EFToRealmMigrator.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 05bc86a7c0..723b3eeada 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -166,11 +166,15 @@ namespace osu.Game.Database backupStorage.Delete(attachment_filename); - using (var zip = ZipArchive.Create()) + try { - zip.AddAllFromDirectory(backupStorage.GetFullPath(string.Empty)); - zip.SaveTo(Path.Combine(backupStorage.GetFullPath(string.Empty), attachment_filename), new ZipWriterOptions(CompressionType.Deflate)); + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(backupStorage.GetFullPath(string.Empty)); + zip.SaveTo(Path.Combine(backupStorage.GetFullPath(string.Empty), attachment_filename), new ZipWriterOptions(CompressionType.Deflate)); + } } + catch { } backupStorage.PresentFileExternally(attachment_filename); From 67ccb879926f309f3b2d3dbd3bc31a2a8760b05e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 14:56:04 +0900 Subject: [PATCH 196/587] Add exception message to discussion template url --- osu.Game/Database/EFToRealmMigrator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 723b3eeada..639bf10e19 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -157,7 +157,7 @@ namespace osu.Game.Database Text = "IMPORTANT: During data migration, some of your data could not be successfully migrated. The previous version has been backed up.\n\nFor further assistance, please open a discussion on github and attach your backup files (click to get started).", Activated = () => { - game.OpenUrlExternally(@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue&body=Please%20drag%20the%20""attach_me.zip""%20file%20here!&category=q-a", true); + game.OpenUrlExternally($@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue ({t.Exception.Message})&body=Please%20drag%20the%20""attach_me.zip""%20file%20here!&category=q-a", true); const string attachment_filename = "attach_me.zip"; const string backup_folder = "backups"; From f30d63107a73c1a4f059366a12483f086e26c22f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 15:19:27 +0900 Subject: [PATCH 197/587] Add `SortID` to `RulesetInfo` to allow stable ordering of rulesets for display --- .../NonVisual/RulesetInfoOrderingTest.cs | 38 +++++++++++++++++++ osu.Game/Rulesets/RulesetInfo.cs | 5 +++ osu.Game/Rulesets/RulesetStore.cs | 5 +-- .../Select/Carousel/CarouselBeatmap.cs | 2 +- .../Select/Carousel/CarouselBeatmapSet.cs | 2 +- 5 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs diff --git a/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs b/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs new file mode 100644 index 0000000000..e7c04d27c1 --- /dev/null +++ b/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.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 NUnit.Framework; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Osu; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class RulesetInfoOrderingTest + { + [Test] + public void TestOrdering() + { + var rulesets = new[] + { + new RulesetInfo("custom2", "Custom Ruleset 2", string.Empty, -1), + new OsuRuleset().RulesetInfo, + new RulesetInfo("custom3", "Custom Ruleset 3", string.Empty, -1), + new RulesetInfo("custom2", "Custom Ruleset 2", string.Empty, -1), + new CatchRuleset().RulesetInfo, + new RulesetInfo("custom3", "Custom Ruleset 3", string.Empty, -1), + }; + + var orderedRulesets = rulesets.OrderBy(r => r.SortID); + + // Ensure all customs are after official. + Assert.That(orderedRulesets.Select(r => r.OnlineID), Is.EqualTo(new[] { 0, 2, -1, -1, -1, -1 })); + + // Ensure customs are grouped next to each other (ie. stably sorted). + Assert.That(orderedRulesets.SkipWhile(r => r.ShortName != "custom2").Skip(1).First().ShortName, Is.EqualTo("custom2")); + Assert.That(orderedRulesets.SkipWhile(r => r.ShortName != "custom3").Skip(1).First().ShortName, Is.EqualTo("custom3")); + } + } +} diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 2e2ec5c024..d721926e95 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets public string InstantiationInfo { get; set; } = string.Empty; + /// + /// A best effort sort ID which provides stable ordering and puts online rulesets before non-online rulesets. + /// + public int SortID => OnlineID >= 0 ? OnlineID : Math.Abs(ShortName.GetHashCode()); + public RulesetInfo(string shortName, string name, string instantiationInfo, int onlineID) { ShortName = shortName; diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 0f518be639..a9fd8f430e 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -163,10 +163,7 @@ namespace osu.Game.Rulesets } } - // add known official rulesets first.. - availableRulesets.AddRange(detachedRulesets.Where(r => r.OnlineID >= 0).OrderBy(r => r.OnlineID)); - // .. then add any customs - availableRulesets.AddRange(detachedRulesets.Where(r => r.OnlineID < 0).OrderBy(r => r.ShortName)); + availableRulesets.AddRange(detachedRulesets.OrderBy(r => r.SortID)); }); } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index e6dee0233f..9d49fdaf6e 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Select.Carousel { default: case SortMode.Difficulty: - int ruleset = BeatmapInfo.RulesetID.CompareTo(otherBeatmap.BeatmapInfo.RulesetID); + int ruleset = BeatmapInfo.Ruleset.SortID.CompareTo(otherBeatmap.BeatmapInfo.Ruleset.SortID); if (ruleset != 0) return ruleset; return BeatmapInfo.StarRating.CompareTo(otherBeatmap.BeatmapInfo.StarRating); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index b2b3b5411c..274be7b73a 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Select.Carousel beatmapSet.Beatmaps .Where(b => !b.Hidden) - .OrderBy(b => b.RulesetID) + .OrderBy(b => b.Ruleset.SortID) .ThenBy(b => b.StarRating) .Select(b => new CarouselBeatmap(b)) .ForEach(AddChild); From e67b1fe0ec9c50b7ddb429edb2ee13d534e881ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 15:25:56 +0900 Subject: [PATCH 198/587] Make `Ruleset.RulesetInfo` get only --- osu.Game/Rulesets/Ruleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index d279f6d6ee..616540b59c 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets [ExcludeFromDynamicCompile] public abstract class Ruleset { - public RulesetInfo RulesetInfo { get; internal set; } + public RulesetInfo RulesetInfo { get; } private static readonly ConcurrentDictionary mod_reference_cache = new ConcurrentDictionary(); From 714177cce13a86d3665d1685831da3a1e30c7382 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 15:23:16 +0900 Subject: [PATCH 199/587] Remove pointless constructor in `RulesetInfo` --- osu.Game.Tests/Database/RealmTest.cs | 2 +- osu.Game/Rulesets/RulesetInfo.cs | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index c2339dd9ad..838759c991 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -114,7 +114,7 @@ namespace osu.Game.Tests.Database } protected static RulesetInfo CreateRuleset() => - new RulesetInfo(0, "osu!", "osu", true); + new RulesetInfo("osu", "osu!", string.Empty, 0) { Available = true }; private class RealmTestGame : Framework.Game { diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 2e2ec5c024..5171f0d63c 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -37,14 +37,6 @@ namespace osu.Game.Rulesets { } - public RulesetInfo(int? onlineID, string name, string shortName, bool available) - { - OnlineID = onlineID ?? -1; - Name = name; - ShortName = shortName; - Available = available; - } - public bool Available { get; set; } public bool Equals(RulesetInfo? other) From 5288eedd311feabd82cebf1ddf19336557197016 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 15:19:48 +0900 Subject: [PATCH 200/587] Update all usages of `RulesetID` and `Ruleset.ID` to use `Ruleset.OnlineID` --- .../ManiaFilterCriteria.cs | 2 +- .../Beatmaps/TaikoBeatmapConverter.cs | 2 +- .../Formats/LegacyBeatmapDecoderTest.cs | 2 +- .../Formats/LegacyBeatmapEncoderTest.cs | 2 +- .../Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- .../Editing/TestSceneDifficultySwitching.cs | 4 +- .../Editing/TestSceneEditorTestGameplay.cs | 2 +- .../TestScenePlayerScoreSubmission.cs | 4 +- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../Visual/Multiplayer/QueueModeTestScene.cs | 4 +- .../TestSceneMultiSpectatorScreen.cs | 2 +- .../Multiplayer/TestSceneMultiplayer.cs | 48 +++++++++---------- .../TestSceneMultiplayerMatchSongSelect.cs | 4 +- .../TestSceneMultiplayerPlaylist.cs | 2 +- .../TestSceneMultiplayerQueueList.cs | 2 +- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 8 ++-- .../SongSelect/TestSceneBeatmapCarousel.cs | 2 +- .../SongSelect/TestScenePlaySongSelect.cs | 26 +++++----- .../Components/TournamentModIcon.cs | 2 +- osu.Game/Beatmaps/BeatmapInfo.cs | 1 - osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs | 2 +- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 8 ++-- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 18 ++++--- osu.Game/Rulesets/RulesetInfo.cs | 6 --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 4 +- .../Select/Carousel/CarouselBeatmap.cs | 2 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- osu.Game/Stores/BeatmapImporter.cs | 4 +- .../Tests/Beatmaps/BeatmapConversionTest.cs | 2 +- .../Tests/Beatmaps/HitObjectSampleTest.cs | 2 +- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 33 files changed, 89 insertions(+), 90 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 0290230490..c8832dfdfb 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania public bool Matches(BeatmapInfo beatmapInfo) { - return !keys.HasFilter || (beatmapInfo.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo))); + return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo))); } public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 613874b7d6..b1d8575de4 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps Beatmap converted = base.ConvertBeatmap(original, cancellationToken); - if (original.BeatmapInfo.RulesetID == 3) + if (original.BeatmapInfo.Ruleset.OnlineID == 3) { // Post processing step to transform mania hit objects with the same start time into strong hits converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x => diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 0459753b28..1edd21b5a9 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(164471, metadata.PreviewTime); Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); - Assert.IsTrue(beatmapInfo.RulesetID == 0); + Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.IsFalse(beatmapInfo.LetterboxInBreaks); Assert.IsFalse(beatmapInfo.SpecialStyle); Assert.IsFalse(beatmapInfo.WidescreenStoryboard); diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index d12da1a22f..d19b3c71f1 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -195,7 +195,7 @@ namespace osu.Game.Tests.Beatmaps.Formats private IBeatmap convert(IBeatmap beatmap) { - switch (beatmap.BeatmapInfo.RulesetID) + switch (beatmap.BeatmapInfo.Ruleset.OnlineID) { case 0: beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo; diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 06ed638e0a..2eb75259d9 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(false, beatmapInfo.SpecialStyle); - Assert.IsTrue(beatmapInfo.RulesetID == 0); + Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs index cf6488f721..81cb286058 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs @@ -82,8 +82,8 @@ namespace osu.Game.Tests.Visual.Editing AddStep("set target difficulty", () => { targetDifficulty = sameRuleset - ? importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.RulesetID == Beatmap.Value.BeatmapInfo.RulesetID) - : importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.RulesetID != Beatmap.Value.BeatmapInfo.RulesetID); + ? importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.Ruleset.ShortName == Beatmap.Value.BeatmapInfo.Ruleset.ShortName) + : importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.Ruleset.ShortName != Beatmap.Value.BeatmapInfo.Ruleset.ShortName); }); switchToDifficulty(() => targetDifficulty); confirmEditingBeatmap(() => targetDifficulty); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 79afc8cf27..2b827bee74 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Editing } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - => beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.RulesetID == 0)); + => beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)); protected override void LoadEditor() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index a4a4f351ec..a9675a2ee2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen); AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true); - AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.RulesetID == new TaikoRuleset().RulesetInfo.ID); + AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.Ruleset.ShortName == new TaikoRuleset().RulesetInfo.ShortName); } [Test] @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen); AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true); - AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.RulesetID == new ManiaRuleset().RulesetInfo.ID); + AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.Ruleset.ShortName == new ManiaRuleset().RulesetInfo.ShortName); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 8b7e1a1d85..0afd2c76dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("import beatmap", () => { importedBeatmap = BeatmapImportHelper.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely(); - importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.RulesetID == 0).OnlineID; + importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.Ruleset.OnlineID == 0).OnlineID; }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 4cd19b53a4..c79395b343 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -60,8 +60,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); - InitialBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); - OtherBeatmap = importedSet.Beatmaps.Last(b => b.RulesetID == 0); + InitialBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); + OtherBeatmap = importedSet.Beatmaps.Last(b => b.Ruleset.OnlineID == 0); }); AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents())); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 9d67742e4d..b84f7760e4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void load() { importedSet = BeatmapImportHelper.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely(); - importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); + importedBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); importedBeatmapId = importedBeatmap.OnlineID; } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 3f151a0ae8..3563869d8b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -234,7 +234,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -317,7 +317,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -339,7 +339,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -372,7 +372,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -392,7 +392,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -414,7 +414,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -453,7 +453,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -492,7 +492,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -531,7 +531,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -565,7 +565,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -605,7 +605,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -625,7 +625,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, AllowedMods = { new OsuModHidden() } } @@ -665,7 +665,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -696,7 +696,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -714,7 +714,7 @@ namespace osu.Game.Tests.Visual.Multiplayer roomManager.ServerSideRooms[0].Playlist.Add(new PlaylistItem { ID = 2, - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, }); }); @@ -742,7 +742,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -778,7 +778,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -817,7 +817,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -828,7 +828,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { - BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID + BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID }))); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); @@ -848,7 +848,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -859,7 +859,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { - BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID + BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID }))); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 5465061891..457b53ae61 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapInfo selectedBeatmap = null; AddStep("select beatmap", - () => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.Where(beatmap => beatmap.RulesetID == new OsuRuleset().LegacyID).ElementAt(1))); + () => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.Where(beatmap => beatmap.Ruleset.OnlineID == new OsuRuleset().LegacyID).ElementAt(1))); AddUntilStep("wait for selection", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap)); AddStep("exit song select", () => songSelect.Exit()); @@ -139,7 +139,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); AddStep("select beatmap", - () => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.RulesetID == new TaikoRuleset().LegacyID))); + () => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == new TaikoRuleset().LegacyID))); AddUntilStep("wait for selection", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap)); AddStep("set mods", () => SelectedMods.Value = new[] { new TaikoModDoubleTime() }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index d970ab6c34..936798e6b4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); - importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); + importedBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); }); AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index d83421ee3a..ddf794b437 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); - importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); + importedBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); }); AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 73c67d26d9..781f0a1824 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } @@ -158,7 +158,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { new PlaylistItem { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 0298c3bea9..830631ebad 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -586,7 +586,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.SelectedBeatmapInfo.RulesetID == 0); + AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 0); AddStep("remove mixed set", () => { diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 458c6130c7..136173c8a7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -325,10 +325,10 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); addRulesetImportStep(1); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 2); changeRuleset(1); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 1); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 1); changeRuleset(0); AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); @@ -341,7 +341,7 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 2); addRulesetImportStep(0); addRulesetImportStep(0); @@ -352,7 +352,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select beatmap/ruleset externally", () => { target = manager.GetAllUsableBeatmapSets() - .Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last(); + .Last(b => b.Beatmaps.Any(bi => bi.Ruleset.OnlineID == 0)).Beatmaps.Last(); Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 0); Beatmap.Value = manager.GetWorkingBeatmap(target); @@ -371,7 +371,7 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 2); addRulesetImportStep(0); addRulesetImportStep(0); @@ -382,7 +382,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select beatmap/ruleset externally", () => { target = manager.GetAllUsableBeatmapSets() - .Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last(); + .Last(b => b.Beatmaps.Any(bi => bi.Ruleset.OnlineID == 0)).Beatmaps.Last(); Beatmap.Value = manager.GetWorkingBeatmap(target); Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 0); @@ -493,9 +493,9 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select beatmap externally", () => { target = manager.GetAllUsableBeatmapSets() - .First(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset)) + .First(b => b.Beatmaps.Any(bi => bi.Ruleset.OnlineID == targetRuleset)) .Beatmaps - .First(bi => bi.RulesetID == targetRuleset); + .First(bi => bi.Ruleset.OnlineID == targetRuleset); Beatmap.Value = manager.GetWorkingBeatmap(target); }); @@ -544,7 +544,7 @@ namespace osu.Game.Tests.Visual.SongSelect { target = manager .GetAllUsableBeatmapSets() - .First(b => b.Beatmaps.Any(bi => bi.RulesetID == 1)) + .First(b => b.Beatmaps.Any(bi => bi.Ruleset.OnlineID == 1)) .Beatmaps.First(); Beatmap.Value = manager.GetWorkingBeatmap(target); @@ -799,8 +799,8 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestChangeRulesetWhilePresentingScore() { - BeatmapInfo getPresentBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 0); - BeatmapInfo getSwitchBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 1); + BeatmapInfo getPresentBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.Ruleset.OnlineID == 0); + BeatmapInfo getSwitchBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.Ruleset.OnlineID == 1); changeRuleset(0); @@ -831,8 +831,8 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestChangeBeatmapWhilePresentingScore() { - BeatmapInfo getPresentBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 0); - BeatmapInfo getSwitchBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 1); + BeatmapInfo getPresentBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.Ruleset.OnlineID == 0); + BeatmapInfo getSwitchBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.Ruleset.OnlineID == 1); changeRuleset(0); diff --git a/osu.Game.Tournament/Components/TournamentModIcon.cs b/osu.Game.Tournament/Components/TournamentModIcon.cs index ed8a36c220..57a0390ac2 100644 --- a/osu.Game.Tournament/Components/TournamentModIcon.cs +++ b/osu.Game.Tournament/Components/TournamentModIcon.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tournament.Components return; } - var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0); + var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.OnlineID ?? 0); var modIcon = ruleset?.CreateInstance().CreateModFromAcronym(modAcronym); if (modIcon == null) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 4e1a34ddbf..3e6e33f1d0 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -155,7 +155,6 @@ namespace osu.Game.Beatmaps [Ignored] public int RulesetID { - get => Ruleset.OnlineID; set { if (!string.IsNullOrEmpty(Ruleset.InstantiationInfo)) diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs index dc8201a402..163da12b2e 100644 --- a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs @@ -30,7 +30,7 @@ namespace osu.Game.Beatmaps { this.beatmap = beatmap; - beatmap.BeatmapInfo.Ruleset = rulesetProvider(beatmap.BeatmapInfo.RulesetID).RulesetInfo; + beatmap.BeatmapInfo.Ruleset = rulesetProvider(beatmap.BeatmapInfo.Ruleset.OnlineID).RulesetInfo; if (beatmapId.HasValue) beatmap.BeatmapInfo.OnlineID = beatmapId.Value; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 8f3f05aa9f..35d1cefeca 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -141,9 +141,11 @@ namespace osu.Game.Beatmaps.Formats break; case @"Mode": - beatmap.BeatmapInfo.RulesetID = Parsing.ParseInt(pair.Value); + int rulesetID = Parsing.ParseInt(pair.Value); - switch (beatmap.BeatmapInfo.RulesetID) + beatmap.BeatmapInfo.RulesetID = rulesetID; + + switch (rulesetID) { case 0: parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion); @@ -397,7 +399,7 @@ namespace osu.Game.Beatmaps.Formats OmitFirstBarLine = omitFirstBarSignature, }; - bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0; + bool isOsuRuleset = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; // scrolling rulesets use effect points rather than difficulty points for scroll speed adjustments. if (!isOsuRuleset) effectPoint.ScrollSpeed = speedMultiplier; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 9d848fd8a4..7ddbc2f768 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -35,6 +35,8 @@ namespace osu.Game.Beatmaps.Formats [CanBeNull] private readonly ISkin skin; + private readonly int onlineRulesetID; + /// /// Creates a new . /// @@ -45,7 +47,9 @@ namespace osu.Game.Beatmaps.Formats this.beatmap = beatmap; this.skin = skin; - if (beatmap.BeatmapInfo.RulesetID < 0 || beatmap.BeatmapInfo.RulesetID > 3) + onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID; + + if (onlineRulesetID < 0 || onlineRulesetID > 3) throw new ArgumentException("Only beatmaps in the osu, taiko, catch, or mania rulesets can be encoded to the legacy beatmap format.", nameof(beatmap)); } @@ -88,7 +92,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}")); writer.WriteLine(FormattableString.Invariant($"SampleSet: {toLegacySampleBank((beatmap.HitObjects.FirstOrDefault()?.SampleControlPoint ?? SampleControlPoint.DEFAULT).SampleBank)}")); writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); - writer.WriteLine(FormattableString.Invariant($"Mode: {beatmap.BeatmapInfo.RulesetID}")); + writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}")); writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}")); // if (beatmap.BeatmapInfo.UseSkinSprites) // writer.WriteLine(@"UseSkinSprites: 1"); @@ -102,7 +106,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(@"EpilepsyWarning: 1"); if (beatmap.BeatmapInfo.CountdownOffset > 0) writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.BeatmapInfo.CountdownOffset}")); - if (beatmap.BeatmapInfo.RulesetID == 3) + if (onlineRulesetID == 3) writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? '1' : '0')}")); if (beatmap.BeatmapInfo.SamplesMatchPlaybackRate) @@ -147,7 +151,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.Difficulty.ApproachRate}")); // Taiko adjusts the slider multiplier (see: LEGACY_TAIKO_VELOCITY_MULTIPLIER) - writer.WriteLine(beatmap.BeatmapInfo.RulesetID == 1 + writer.WriteLine(onlineRulesetID == 1 ? FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}") : FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier}")); @@ -179,7 +183,7 @@ namespace osu.Game.Beatmaps.Formats SampleControlPoint lastRelevantSamplePoint = null; DifficultyControlPoint lastRelevantDifficultyPoint = null; - bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0; + bool isOsuRuleset = onlineRulesetID == 0; // iterate over hitobjects and pull out all required sample and difficulty changes extractDifficultyControlPoints(beatmap.HitObjects); @@ -318,7 +322,7 @@ namespace osu.Game.Beatmaps.Formats { Vector2 position = new Vector2(256, 192); - switch (beatmap.BeatmapInfo.RulesetID) + switch (onlineRulesetID) { case 0: case 2: @@ -372,7 +376,7 @@ namespace osu.Game.Beatmaps.Formats break; case IHasDuration _: - if (beatmap.BeatmapInfo.RulesetID == 3) + if (onlineRulesetID == 3) type |= LegacyHitObjectType.Hold; else type |= LegacyHitObjectType.Spinner; diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 2e2ec5c024..e2007c910e 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -98,11 +98,5 @@ namespace osu.Game.Rulesets return ruleset; } - - #region Compatibility properties - - public int ID => OnlineID; - - #endregion } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 2902ff7848..9460ec680c 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.BeatmapInfo.RulesetID < 0 || score.ScoreInfo.BeatmapInfo.RulesetID > 3) + if (score.ScoreInfo.BeatmapInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.BeatmapInfo.Ruleset.OnlineID > 3) throw new ArgumentException("Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b42f629aad..55b01bd8ab 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -338,7 +338,7 @@ namespace osu.Game.Screens.Edit public EditorState GetState([CanBeNull] BeatmapInfo nextBeatmap = null) => new EditorState { Time = clock.CurrentTimeAccurate, - ClipboardContent = nextBeatmap == null || editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? Clipboard.Content.Value : string.Empty + ClipboardContent = nextBeatmap == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextBeatmap.Ruleset.ShortName ? Clipboard.Content.Value : string.Empty }; /// @@ -780,7 +780,7 @@ namespace osu.Game.Screens.Edit var difficultyItems = new List(); - foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.RulesetID).OrderBy(group => group.Key)) + foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset.ShortName).OrderBy(group => group.Key)) { if (difficultyItems.Count > 0) difficultyItems.Add(new EditorMenuItemSpacer()); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index e6dee0233f..3962f7b26f 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select.Carousel bool match = criteria.Ruleset == null || BeatmapInfo.Ruleset.ShortName == criteria.Ruleset.ShortName || - (BeatmapInfo.RulesetID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps); + (BeatmapInfo.Ruleset.OnlineID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps); if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index f25997650b..2070e53257 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -155,7 +155,7 @@ namespace osu.Game.Screens.Select.Leaderboards var scores = r.All() .AsEnumerable() // TODO: update to use a realm filter directly (or at least figure out the beatmap part to reduce scope). - .Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.OnlineID == ruleset.Value.ID); + .Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.ShortName == ruleset.Value.ShortName); if (filterMods && !mods.Value.Any()) { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 837f30eb2b..10150fcd9f 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -502,7 +502,7 @@ namespace osu.Game.Screens.Select // clear pending task immediately to track any potential nested debounce operation. selectionChangedDebounce = null; - Logger.Log($"updating selection with beatmap:{beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ID.ToString() ?? "null"}"); + Logger.Log($"updating selection with beatmap:{beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ShortName ?? "null"}"); if (transferRulesetValue()) { diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index a6f20c8d4f..709dd67087 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -219,11 +219,11 @@ namespace osu.Game.Stores var decodedInfo = decoded.BeatmapInfo; var decodedDifficulty = decodedInfo.Difficulty; - var ruleset = realm.All().FirstOrDefault(r => r.OnlineID == decodedInfo.RulesetID); + var ruleset = realm.All().FirstOrDefault(r => r.OnlineID == decodedInfo.Ruleset.OnlineID); if (ruleset?.Available != true) { - Logger.Log($"Skipping import of {file.Filename} due to missing local ruleset {decodedInfo.RulesetID}.", LoggingTarget.Database); + Logger.Log($"Skipping import of {file.Filename} due to missing local ruleset {decodedInfo.Ruleset.OnlineID}.", LoggingTarget.Database); continue; } diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 897d4363f1..8d622955b7 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -175,7 +175,7 @@ namespace osu.Game.Tests.Beatmaps var beatmap = decoder.Decode(stream); var rulesetInstance = CreateRuleset(); - beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo(); + beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.Ruleset.OnlineID == rulesetInstance.RulesetInfo.OnlineID ? rulesetInstance.RulesetInfo : new RulesetInfo(); return beatmap; } diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index f7e154b5e7..2a3e51b4f5 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Beatmaps currentTestBeatmap = Decoder.GetDecoder(reader).Decode(reader); // populate ruleset for beatmap converters that require it to be present. - var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.RulesetID); + var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID); Debug.Assert(ruleset != null); diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 42e96f80ca..ec02655544 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -282,7 +282,7 @@ namespace osu.Game.Tests.Visual Status = beatmap.Status, Checksum = beatmap.MD5Hash, AuthorID = beatmap.Metadata.Author.OnlineID, - RulesetID = beatmap.RulesetID, + RulesetID = beatmap.Ruleset.OnlineID, StarRating = beatmap.StarRating, DifficultyName = beatmap.DifficultyName, } From 5637fd64d641dcf147d7f9e2d17532bf351264ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 15:59:20 +0900 Subject: [PATCH 201/587] Perform ordering using `IComparable` instead --- .../NonVisual/RulesetInfoOrderingTest.cs | 2 +- osu.Game/Rulesets/EFRulesetInfo.cs | 2 ++ osu.Game/Rulesets/IRulesetInfo.cs | 2 +- osu.Game/Rulesets/RulesetInfo.cs | 19 ++++++++++++++----- osu.Game/Rulesets/RulesetStore.cs | 2 +- .../Select/Carousel/CarouselBeatmap.cs | 2 +- .../Select/Carousel/CarouselBeatmapSet.cs | 2 +- 7 files changed, 21 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs b/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs index e7c04d27c1..ae999d08d5 100644 --- a/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs +++ b/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.NonVisual new RulesetInfo("custom3", "Custom Ruleset 3", string.Empty, -1), }; - var orderedRulesets = rulesets.OrderBy(r => r.SortID); + var orderedRulesets = rulesets.OrderBy(r => r); // Ensure all customs are after official. Assert.That(orderedRulesets.Select(r => r.OnlineID), Is.EqualTo(new[] { 0, 2, -1, -1, -1, -1 })); diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs index 9559cfa00c..b4a64fe932 100644 --- a/osu.Game/Rulesets/EFRulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -42,6 +42,8 @@ namespace osu.Game.Rulesets public bool Equals(EFRulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + public int CompareTo(RulesetInfo other) => ID?.CompareTo(other.ID) ?? -1; + public override bool Equals(object obj) => obj is EFRulesetInfo rulesetInfo && Equals(rulesetInfo); public bool Equals(IRulesetInfo other) => other is RulesetInfo b && Equals(b); diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index 6599e0d59d..44731a2495 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets /// /// A representation of a ruleset's metadata. /// - public interface IRulesetInfo : IHasOnlineID, IEquatable + public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable { /// /// The user-exposed name of this ruleset. diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index d721926e95..896b9ee279 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -24,11 +24,6 @@ namespace osu.Game.Rulesets public string InstantiationInfo { get; set; } = string.Empty; - /// - /// A best effort sort ID which provides stable ordering and puts online rulesets before non-online rulesets. - /// - public int SortID => OnlineID >= 0 ? OnlineID : Math.Abs(ShortName.GetHashCode()); - public RulesetInfo(string shortName, string name, string instantiationInfo, int onlineID) { ShortName = shortName; @@ -62,6 +57,20 @@ namespace osu.Game.Rulesets public bool Equals(IRulesetInfo? other) => other is RulesetInfo b && Equals(b); + public int CompareTo(RulesetInfo other) + { + // Official rulesets are always given precedence for the time being. + if (OnlineID >= 0) + { + if (other.OnlineID >= 0) + return OnlineID.CompareTo(other.OnlineID); + + return -1; + } + + return string.Compare(ShortName, other.ShortName, StringComparison.Ordinal); + } + public override int GetHashCode() { // Importantly, ignore the underlying realm hash code, as it will usually not match. diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index a9fd8f430e..d017d54ed9 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -163,7 +163,7 @@ namespace osu.Game.Rulesets } } - availableRulesets.AddRange(detachedRulesets.OrderBy(r => r.SortID)); + availableRulesets.AddRange(detachedRulesets.OrderBy(r => r)); }); } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 9d49fdaf6e..0045e7ba4f 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Select.Carousel { default: case SortMode.Difficulty: - int ruleset = BeatmapInfo.Ruleset.SortID.CompareTo(otherBeatmap.BeatmapInfo.Ruleset.SortID); + int ruleset = BeatmapInfo.Ruleset.CompareTo(otherBeatmap.BeatmapInfo.Ruleset); if (ruleset != 0) return ruleset; return BeatmapInfo.StarRating.CompareTo(otherBeatmap.BeatmapInfo.StarRating); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 274be7b73a..fc4b6c27f3 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Select.Carousel beatmapSet.Beatmaps .Where(b => !b.Hidden) - .OrderBy(b => b.Ruleset.SortID) + .OrderBy(b => b.Ruleset) .ThenBy(b => b.StarRating) .Select(b => new CarouselBeatmap(b)) .ForEach(AddChild); From b87d1a61a8d2d2d48674ba3bed92568cabef7c25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 16:39:36 +0900 Subject: [PATCH 202/587] Fix `ButtonSystem` null reference crash due to missing null check in delayed animations ```csharp [runtime] 2022-01-27 07:36:34 [error]: System.NullReferenceException: Object reference not set to an instance of an object. [runtime] 2022-01-27 07:36:34 [error]: at osu.Game.Screens.Menu.ButtonSystem.<>c__DisplayClass56_0.b__1() in /Users/dean/Projects/osu/osu.Game/Screens/Menu/ButtonSystem.cs:line 357 [runtime] 2022-01-27 07:36:34 [error]: at osu.Framework.Threading.ScheduledDelegate.RunTaskInternal() [runtime] 2022-01-27 07:36:34 [error]: at osu.Framework.Threading.Scheduler.Update() [runtime] 2022-01-27 07:36:34 [error]: at osu.Framework.Graphics.Drawable.UpdateSubTree() ``` --- osu.Game/Screens/Menu/ButtonSystem.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 32731407fd..b03425fef4 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -49,6 +50,7 @@ namespace osu.Game.Screens.Menu public const float BUTTON_WIDTH = 140f; public const float WEDGE_WIDTH = 20; + [CanBeNull] private OsuLogo logo; /// @@ -328,9 +330,9 @@ namespace osu.Game.Screens.Menu game?.Toolbar.Hide(); - logo.ClearTransforms(targetMember: nameof(Position)); - logo.MoveTo(new Vector2(0.5f), 800, Easing.OutExpo); - logo.ScaleTo(1, 800, Easing.OutExpo); + logo?.ClearTransforms(targetMember: nameof(Position)); + logo?.MoveTo(new Vector2(0.5f), 800, Easing.OutExpo); + logo?.ScaleTo(1, 800, Easing.OutExpo); }, buttonArea.Alpha * 150); break; @@ -354,7 +356,7 @@ namespace osu.Game.Screens.Menu logoDelayedAction = Scheduler.AddDelayed(() => { if (impact) - logo.Impact(); + logo?.Impact(); game?.Toolbar.Show(); }, 200); From 0a45aa80cb011e658dc1d7c3504d2fe4ef12f079 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 16:16:49 +0900 Subject: [PATCH 203/587] Remove unnecessary double-schedule in `UpdateBeatmapSet` --- osu.Game/Screens/Select/BeatmapCarousel.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index dff2c598c3..94e520ba1c 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -311,13 +311,10 @@ namespace osu.Game.Screens.Select itemsCache.Invalidate(); - Schedule(() => - { - if (!Scroll.UserScrolling) - ScrollToSelected(true); + if (!Scroll.UserScrolling) + ScrollToSelected(true); - BeatmapSetsChanged?.Invoke(); - }); + BeatmapSetsChanged?.Invoke(); }); /// From 449e9bcf5c123e1e1c92534f8f3a710412a5028d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 16:17:38 +0900 Subject: [PATCH 204/587] Ensure beatmap carousel scroll position is maintained during deletion operations --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 94e520ba1c..c27915c383 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -286,6 +286,9 @@ namespace osu.Game.Screens.Select root.RemoveChild(existingSet); itemsCache.Invalidate(); + + if (!Scroll.UserScrolling) + ScrollToSelected(true); }); public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => From f2cecad83b2e1df2b2e15c5cd49e04534cf55dca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 16:38:02 +0900 Subject: [PATCH 205/587] Add failing test coverage showing carousel deletions don't keep scroll position --- .../SongSelect/TestSceneBeatmapCarousel.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 0298c3bea9..2f2001c4f2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets; @@ -40,6 +41,36 @@ namespace osu.Game.Tests.Visual.SongSelect this.rulesets = rulesets; } + [Test] + public void TestScrollPositionMaintainedOnAdd() + { + loadBeatmaps(count: 1, randomDifficulties: false); + + for (int i = 0; i < 10; i++) + { + AddRepeatStep("Add some sets", () => carousel.UpdateBeatmapSet(TestResources.CreateTestBeatmapSetInfo()), 4); + + checkSelectionIsCentered(); + } + } + + [Test] + public void TestScrollPositionMaintainedOnDelete() + { + loadBeatmaps(count: 50, randomDifficulties: false); + + for (int i = 0; i < 10; i++) + { + AddRepeatStep("Remove some sets", () => + carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item) + .OfType() + .OrderBy(item => item.GetHashCode()) + .First(item => item.State.Value != CarouselItemState.Selected && item.Visible).BeatmapSet), 4); + + checkSelectionIsCentered(); + } + } + [Test] public void TestManyPanels() { @@ -813,6 +844,18 @@ namespace osu.Game.Tests.Visual.SongSelect carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count); } + private void checkSelectionIsCentered() + { + AddAssert("Selected panel is centered", () => + { + return Precision.AlmostEquals( + carousel.ScreenSpaceDrawQuad.Centre, + carousel.Items + .First(i => i.Item.State.Value == CarouselItemState.Selected) + .ScreenSpaceDrawQuad.Centre, 100); + }); + } + private void checkNoSelection() => AddAssert("Selection is null", () => currentSelection == null); private void nextRandom() => From 3ae5973ab7de887e1908430d3601fbc18301489e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 17:08:12 +0900 Subject: [PATCH 206/587] Fix compilation error due to commit split --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 3962f7b26f..1f346d19f0 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Select.Carousel { default: case SortMode.Difficulty: - int ruleset = BeatmapInfo.RulesetID.CompareTo(otherBeatmap.BeatmapInfo.RulesetID); + int ruleset = BeatmapInfo.Ruleset.OnlineID.CompareTo(otherBeatmap.BeatmapInfo.Ruleset.OnlineID); if (ruleset != 0) return ruleset; return BeatmapInfo.StarRating.CompareTo(otherBeatmap.BeatmapInfo.StarRating); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index b2b3b5411c..6156fea769 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Select.Carousel beatmapSet.Beatmaps .Where(b => !b.Hidden) - .OrderBy(b => b.RulesetID) + .OrderBy(b => b.Ruleset.OnlineID) .ThenBy(b => b.StarRating) .Select(b => new CarouselBeatmap(b)) .ForEach(AddChild); From 831fa44433059078f0c96d06cf0e69b8686e5d8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 19:35:04 +0900 Subject: [PATCH 207/587] Fix song select tests not waiting for beatmap imports to arrive After the change to realm, notification fires could take a frame or two. We aren't accounting for this. Fixes test failures like https://github.com/ppy/osu/runs/4963255990?check_suite_focus=true --- .../SongSelect/TestScenePlaySongSelect.cs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 458c6130c7..ca0f2b8012 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -13,6 +13,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; @@ -73,6 +74,17 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("delete all beatmaps", () => manager?.Delete()); } + public override void TearDownSteps() + { + base.TearDownSteps(); + + AddStep("remove song select", () => + { + songSelect?.Expire(); + songSelect = null; + }); + } + [Test] public void TestSingleFilterOnEnter() { @@ -870,9 +882,16 @@ namespace osu.Game.Tests.Visual.SongSelect return set.ChildrenOfType().ToList().FindIndex(i => i == icon); } - private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id)); + private void addRulesetImportStep(int id) + { + Live imported = null; + AddStep($"import test map for ruleset {id}", () => imported = importForRuleset(id)); + // This is specifically for cases where the add is happening post song select load. + // For cases where song select is null, the assertions are provided by the load checks. + AddUntilStep("wait for imported to arrive in carousel", () => songSelect == null || songSelect.Carousel.BeatmapSets.Any(s => s.ID == imported?.ID)); + } - private void importForRuleset(int id) => manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == id).ToArray())); + private Live importForRuleset(int id) => manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == id).ToArray())); private void checkMusicPlaying(bool playing) => AddUntilStep($"music {(playing ? "" : "not ")}playing", () => music.IsPlaying == playing); From 1bdf16494baa48fd15715ece296b198f6f60b64f Mon Sep 17 00:00:00 2001 From: Spooghetti420 Date: Thu, 27 Jan 2022 11:35:31 +0000 Subject: [PATCH 208/587] Add No Long Notes mod --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 3 +- .../Mods/ManiaModNoLongNotes.cs | 80 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 6fc7dc018b..3471f4ca66 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -243,7 +243,8 @@ namespace osu.Game.Rulesets.Mania new ManiaModDifficultyAdjust(), new ManiaModClassic(), new ManiaModInvert(), - new ManiaModConstantSpeed() + new ManiaModConstantSpeed(), + new ManiaModNoLongNotes() }; case ModType.Automation: diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs new file mode 100644 index 0000000000..7295a9eb6e --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs @@ -0,0 +1,80 @@ +// 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.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mods; +using osu.Framework.Graphics.Sprites; +using System; +using System.Collections.Generic; +using osu.Game.Audio; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Framework.Utils; +using osu.Game.Overlays.Settings; +using osu.Framework.Bindables; +using osu.Game.Configuration; +using osu.Game.Graphics; + + + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModNoLongNotes : Mod, IApplicableAfterBeatmapConversion + { + + public override string Name => "No Long Notes"; + public override string Acronym => "NL"; + public override double ScoreMultiplier => 1; + public override string Description => @"Turns all held notes into tap notes. No coordination required."; + public override IconUsage? Icon => FontAwesome.Solid.DotCircle; + public override ModType Type => ModType.Conversion; + + [SettingSource("Add end notes", "Also add a note at the end of a held note")] + public BindableBool AddEndNotes { get; } = new BindableBool + { + Default = true, + Value = true + }; + + [SettingSource("Length threshold", "Only add an end note for held notes longer than this threshold (in milliseconds)")] + public BindableNumber Threshold { get; } = new BindableDouble + { + MinValue = 1.0, + MaxValue = 1990.0, + Default = 200.0, + Value = 200.0, + Precision = 1.0, + }; + public void ApplyToBeatmap(IBeatmap beatmap) + { + var maniaBeatmap = (ManiaBeatmap)beatmap; + + var newObjects = new List(); + beatmap.HitObjects.OfType().ForEach(h => + { + // Add a note for the beginning of the hold note + newObjects.Add(new Note + { + Column = h.Column, + StartTime = h.StartTime, + Samples = h.Samples + }); + + // Don't add an end note if the duration is below the threshold, or end notes are disabled + if (AddEndNotes.Value && h.Duration > Threshold.Value) + { + newObjects.Add(new Note + { + Column = h.Column, + StartTime = h.EndTime, + Samples = h.Samples + }); + } + }); + + maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType().Concat(newObjects).OrderBy(h => h.StartTime).ToList(); + } + } +} From 33b7bdcf8260cf0d9090d9edaa186729682b8b3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 21:49:33 +0900 Subject: [PATCH 209/587] Update pointless `CompareTo` implementation --- osu.Game/Rulesets/EFRulesetInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs index b4a64fe932..ba56adac49 100644 --- a/osu.Game/Rulesets/EFRulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets public bool Equals(EFRulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; - public int CompareTo(RulesetInfo other) => ID?.CompareTo(other.ID) ?? -1; + public int CompareTo(RulesetInfo other) => OnlineID.CompareTo(other.OnlineID); public override bool Equals(object obj) => obj is EFRulesetInfo rulesetInfo && Equals(rulesetInfo); From 2cc69d6b19b2682db3a7c6ec8dd79ccc1421a0d1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 27 Jan 2022 15:57:56 +0300 Subject: [PATCH 210/587] Replace `IsControlDragged` with an abstract `ShouldBeExpanded` --- osu.Game/Overlays/ExpandableSlider.cs | 2 +- osu.Game/Overlays/ExpandingControlContainer.cs | 8 +++++++- osu.Game/Overlays/IExpandable.cs | 11 +++++++++++ osu.Game/Overlays/IExpandableControl.cs | 4 ---- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs index 524485d806..d346b9b22c 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays public BindableBool Expanded { get; } = new BindableBool(); - public bool IsControlDragged => slider.IsDragged; + bool IExpandable.ShouldBeExpanded => IsHovered || slider.IsDragged; public override bool HandlePositionalInput => true; diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs index fb6a71ba97..859e4bcd25 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -118,6 +118,12 @@ namespace osu.Game.Overlays /// /// Whether the given control is currently active, by checking whether it's hovered or dragged. /// - private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is IExpandableControl expandable && expandable.IsControlDragged); + private bool isControlActive(TControl control) + { + if (control is IExpandable expandable) + return expandable.ShouldBeExpanded; + + return control.IsHovered || control.IsDragged; + } } } diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Overlays/IExpandable.cs index 770ac97847..f998fc7b9f 100644 --- a/osu.Game/Overlays/IExpandable.cs +++ b/osu.Game/Overlays/IExpandable.cs @@ -3,6 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays { @@ -15,5 +16,15 @@ namespace osu.Game.Overlays /// Whether this drawable is in an expanded state. /// BindableBool Expanded { get; } + + /// + /// Whether this drawable should be/stay expanded by a parenting . + /// By default, this is when this drawable is in a hovered or dragged state. + /// + /// + /// This is defined for certain controls which may have a child handling dragging instead. + /// (e.g. in which dragging is handled by their underlying control). + /// + bool ShouldBeExpanded => IsHovered || IsDragged; } } diff --git a/osu.Game/Overlays/IExpandableControl.cs b/osu.Game/Overlays/IExpandableControl.cs index fae07ae23b..2cda6f467b 100644 --- a/osu.Game/Overlays/IExpandableControl.cs +++ b/osu.Game/Overlays/IExpandableControl.cs @@ -8,9 +8,5 @@ namespace osu.Game.Overlays /// public interface IExpandableControl : IExpandable { - /// - /// Returns whether the UI control is currently in a dragged state. - /// - bool IsControlDragged { get; } } } From 2d1a8a9d492258f760ed9d1475135862d76f483f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 21:58:04 +0900 Subject: [PATCH 211/587] Use a more correct `CompareTo` implementation for ruleset ordering --- osu.Game/Rulesets/RulesetInfo.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index dd47a19f32..0a0941d1ff 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -51,14 +51,14 @@ namespace osu.Game.Rulesets public int CompareTo(RulesetInfo other) { + if (OnlineID >= 0 && other.OnlineID >= 0) + return OnlineID.CompareTo(other.OnlineID); + // Official rulesets are always given precedence for the time being. if (OnlineID >= 0) - { - if (other.OnlineID >= 0) - return OnlineID.CompareTo(other.OnlineID); - return -1; - } + if (other.OnlineID >= 0) + return 1; return string.Compare(ShortName, other.ShortName, StringComparison.Ordinal); } From fae4f8bd8e17f7a5df716b8a19ca731a229c714f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jan 2022 21:59:44 +0900 Subject: [PATCH 212/587] Move nulling of previous `songSelect` to `SetUpSteps` instead --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index ca0f2b8012..0c452f857f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -69,22 +69,13 @@ namespace osu.Game.Tests.Visual.SongSelect { Ruleset.Value = new OsuRuleset().RulesetInfo; Beatmap.SetDefault(); + + songSelect = null; }); AddStep("delete all beatmaps", () => manager?.Delete()); } - public override void TearDownSteps() - { - base.TearDownSteps(); - - AddStep("remove song select", () => - { - songSelect?.Expire(); - songSelect = null; - }); - } - [Test] public void TestSingleFilterOnEnter() { From 81461be49fede927581065f46f03331d69d93e8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 00:14:18 +0900 Subject: [PATCH 213/587] Skip beatmap imports where ruleset is not present in realm Closes #16651. When a ruleset is not available, the `Find` call would return null. When a null is passed to the constructor, `BeatmapInfo` would create an "osu" ruleset, which tries to get stored to realm and fails on duplicate primary key. Probably need to add better safeties against this (or change that constructor...) but this will fix the migration process. Probably not serious enough to pull the build. This only affects rulesets like karaoke which have custom beatmaps. --- osu.Game/Database/EFToRealmMigrator.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 639bf10e19..9fb0130619 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -232,6 +232,7 @@ namespace osu.Game.Database var transaction = r.BeginWrite(); int written = 0; + int missing = 0; try { @@ -261,6 +262,12 @@ namespace osu.Game.Database var ruleset = r.Find(beatmap.RulesetInfo.ShortName); var metadata = getBestMetadata(beatmap.Metadata, beatmapSet.Metadata); + if (ruleset == null) + { + log($"Skipping {++missing} beatmaps with missing ruleset"); + continue; + } + var realmBeatmap = new BeatmapInfo(ruleset, new BeatmapDifficulty(beatmap.BaseDifficulty), metadata) { DifficultyName = beatmap.DifficultyName, From 942ea896f1dfab080c624e57b8c90b30844e56ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 01:20:32 +0900 Subject: [PATCH 214/587] Skip scores missing beatmaps during realm migration --- osu.Game/Database/EFToRealmMigrator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 9fb0130619..a53704df9d 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -374,12 +374,12 @@ namespace osu.Game.Database log($"Migrated {written}/{count} scores..."); } - var beatmap = r.All().First(b => b.Hash == score.BeatmapInfo.Hash); + var beatmap = r.All().FirstOrDefault(b => b.Hash == score.BeatmapInfo.Hash); var ruleset = r.Find(score.Ruleset.ShortName); - if (ruleset == null) + if (beatmap == null || ruleset == null) { - log($"Skipping {++missing} scores with missing ruleset"); + log($"Skipping {++missing} scores with missing ruleset or beatmap"); continue; } From c0b2f8bd01b62c2bbd0366c8d97eb883e95b9349 Mon Sep 17 00:00:00 2001 From: Spooghetti420 Date: Thu, 27 Jan 2022 16:21:38 +0000 Subject: [PATCH 215/587] Fix newline style in mod --- osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs index 7295a9eb6e..56522a4587 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs @@ -25,10 +25,15 @@ namespace osu.Game.Rulesets.Mania.Mods { public override string Name => "No Long Notes"; + public override string Acronym => "NL"; + public override double ScoreMultiplier => 1; + public override string Description => @"Turns all held notes into tap notes. No coordination required."; + public override IconUsage? Icon => FontAwesome.Solid.DotCircle; + public override ModType Type => ModType.Conversion; [SettingSource("Add end notes", "Also add a note at the end of a held note")] From 400633bd99a8cb86f647437d18f25ee4c702232b Mon Sep 17 00:00:00 2001 From: Spooghetti420 Date: Thu, 27 Jan 2022 16:23:09 +0000 Subject: [PATCH 216/587] Add another newline --- osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs index 56522a4587..d10003b783 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs @@ -52,6 +52,7 @@ namespace osu.Game.Rulesets.Mania.Mods Value = 200.0, Precision = 1.0, }; + public void ApplyToBeatmap(IBeatmap beatmap) { var maniaBeatmap = (ManiaBeatmap)beatmap; From 9deeaee40481e7702c4b0e143ec177e755928dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jan 2022 16:57:38 +0100 Subject: [PATCH 217/587] Fix tournament client not loading Caused by a `LoadComponentsAsync()` call being fired from a worker thread, which will throw exceptions since the recent addition of safety checks around that method. --- osu.Game.Tournament/TournamentGame.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 5d613894d4..7967f54b49 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -61,18 +61,15 @@ namespace osu.Game.Tournament loadingSpinner.Show(); - BracketLoadTask.ContinueWith(t => + BracketLoadTask.ContinueWith(t => Schedule(() => { if (t.IsFaulted) { - Schedule(() => - { - loadingSpinner.Hide(); - loadingSpinner.Expire(); + loadingSpinner.Hide(); + loadingSpinner.Expire(); - Logger.Error(t.Exception, "Couldn't load bracket with error"); - Add(new WarningBox($"Your {BRACKET_FILENAME} file could not be parsed. Please check runtime.log for more details.")); - }); + Logger.Error(t.Exception, "Couldn't load bracket with error"); + Add(new WarningBox($"Your {BRACKET_FILENAME} file could not be parsed. Please check runtime.log for more details.")); return; } @@ -143,7 +140,7 @@ namespace osu.Game.Tournament windowMode.Value = WindowMode.Windowed; }), true); }); - }); + })); } } } From 7dc3940dee206b6ebec790078c4eefa6ee78b464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jan 2022 21:41:09 +0100 Subject: [PATCH 218/587] Add test coverage for preserving legacy beatmap info defaults --- .../Formats/LegacyBeatmapDecoderTest.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 1edd21b5a9..3a6051ed43 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -794,5 +794,32 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(path.Distance, Is.EqualTo(1)); } } + + [Test] + public void TestLegacyDefaultsPreserved() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var memoryStream = new MemoryStream()) + using (var stream = new LineBufferedReader(memoryStream)) + { + var decoded = decoder.Decode(stream); + + Assert.Multiple(() => + { + Assert.That(decoded.BeatmapInfo.AudioLeadIn, Is.EqualTo(0)); + Assert.That(decoded.BeatmapInfo.StackLeniency, Is.EqualTo(0.7f)); + Assert.That(decoded.BeatmapInfo.SpecialStyle, Is.False); + Assert.That(decoded.BeatmapInfo.LetterboxInBreaks, Is.False); + Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); + Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); + Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); + Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.Normal)); + Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); + Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1)); + Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0)); + }); + } + } } } From 1b8136e3e0fa023a613dd0b66602b9b943db83d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jan 2022 21:41:30 +0100 Subject: [PATCH 219/587] Change some `BeatmapInfo` defaults in a backwards compatible manner --- osu.Game/Beatmaps/BeatmapInfo.cs | 4 ++-- osu.Game/Beatmaps/BeatmapManager.cs | 9 +-------- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 3e6e33f1d0..e4bfd768b7 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -99,11 +99,11 @@ namespace osu.Game.Beatmaps public bool LetterboxInBreaks { get; set; } - public bool WidescreenStoryboard { get; set; } + public bool WidescreenStoryboard { get; set; } = true; public bool EpilepsyWarning { get; set; } - public bool SamplesMatchPlaybackRate { get; set; } + public bool SamplesMatchPlaybackRate { get; set; } = true; public double DistanceSpacing { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 414b7cd12b..e4fdb3d471 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -90,14 +90,7 @@ namespace osu.Game.Beatmaps { Beatmaps = { - new BeatmapInfo - { - Difficulty = new BeatmapDifficulty(), - Ruleset = ruleset, - Metadata = metadata, - WidescreenStoryboard = true, - SamplesMatchPlaybackRate = true, - } + new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) } }; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 35d1cefeca..01ba0fcc9f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -56,6 +56,8 @@ namespace osu.Game.Beatmaps.Formats this.beatmap = beatmap; this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion; + applyLegacyDefaults(this.beatmap.BeatmapInfo); + base.ParseStreamInto(stream, beatmap); flushPendingPoints(); @@ -70,6 +72,19 @@ namespace osu.Game.Beatmaps.Formats hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.Difficulty); } + /// + /// Some `BeatmapInfo` members have default values that differ from the default values used by stable. + /// In addition, legacy beatmaps will sometimes not contain some configuration keys, in which case + /// the legacy default values should be used. + /// This method's intention is to restore those legacy defaults. + /// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29 + /// + private void applyLegacyDefaults(BeatmapInfo beatmapInfo) + { + beatmapInfo.WidescreenStoryboard = false; + beatmapInfo.SamplesMatchPlaybackRate = false; + } + protected override bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(' ') || line.StartsWith('_'); protected override void ParseLine(Beatmap beatmap, Section section, string line) From 6674567af1d678f33eb7c54920e3f11cba7301a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jan 2022 21:51:51 +0100 Subject: [PATCH 220/587] Use -1 as the default preview time globally in metadata --- osu.Game/Beatmaps/BeatmapMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index cb38373bd3..1514d3af7a 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -39,7 +39,7 @@ namespace osu.Game.Beatmaps /// The time in milliseconds to begin playing the track for preview purposes. /// If -1, the track should begin playing at 40% of its length. /// - public int PreviewTime { get; set; } + public int PreviewTime { get; set; } = -1; public string AudioFile { get; set; } = string.Empty; public string BackgroundFile { get; set; } = string.Empty; From 3e068e564d79d6b446b93a69904268abd19ba194 Mon Sep 17 00:00:00 2001 From: Spooghetti420 Date: Thu, 27 Jan 2022 20:56:51 +0000 Subject: [PATCH 221/587] Update mod per discussion + create test --- .../Mods/TestSceneManiaModNoHolds.cs | 52 +++++++++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Mods/ManiaModNoHolds.cs | 75 +++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.cs create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.cs new file mode 100644 index 0000000000..411f891f43 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.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.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Tests.Visual; +using osu.Game.Rulesets.Mania.Tests; +using System.Collections.Generic; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Beatmaps; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public class TestSceneManiaModNoHolds : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Test] + public void TestMapHasNoHeldNotes() + { + var testBeatmap = createBeatmap(); + Assert.That(!testBeatmap.HitObjects.OfType().Any()); + } + + + private static IBeatmap createBeatmap() + { + var beatmap = createRawBeatmap(); + var noHoldsMod = new ManiaModNoHolds(); + + foreach (var hitObject in beatmap.HitObjects) + hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + noHoldsMod.ApplyToBeatmap(beatmap); + + return beatmap; + } + private static IBeatmap createRawBeatmap() + { + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }); + beatmap.HitObjects.Add(new Note { StartTime = 1000 }); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, EndTime = 3000 }); + return beatmap; + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 3471f4ca66..55ca1a465e 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Mania new ManiaModClassic(), new ManiaModInvert(), new ManiaModConstantSpeed(), - new ManiaModNoLongNotes() + new ManiaModNoHolds() }; case ModType.Automation: diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs new file mode 100644 index 0000000000..0d5ca44898 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mods; +using osu.Framework.Graphics.Sprites; +using System; +using System.Collections.Generic; +using osu.Game.Audio; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Framework.Utils; +using osu.Game.Overlays.Settings; +using osu.Framework.Bindables; +using osu.Game.Configuration; +using osu.Game.Graphics; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModNoHolds : Mod, IApplicableAfterBeatmapConversion + { + public override string Name => "No Holds"; + + public override string Acronym => "NH"; + + public override double ScoreMultiplier => 1; + + public override string Description => @"Turns all hold notes into normal notes. No coordination required."; + + public override IconUsage? Icon => FontAwesome.Solid.DotCircle; + + public override ModType Type => ModType.Conversion; + + [SettingSource("Add end notes", "Also add a note at the end of a hold note")] + public BindableBool AddEndNotes { get; } = new BindableBool + { + Default = true, + Value = true + }; + + public void ApplyToBeatmap(IBeatmap beatmap) + { + var maniaBeatmap = (ManiaBeatmap)beatmap; + + var newObjects = new List(); + foreach (var h in beatmap.HitObjects.OfType()) + { + // Add a note for the beginning of the hold note + newObjects.Add(new Note + { + Column = h.Column, + StartTime = h.StartTime, + Samples = h.GetNodeSamples(0) + }); + + // Don't add an end note if the duration is shorter than some threshold, or end notes are disabled + if (AddEndNotes.Value && h.Duration > 200) + { + newObjects.Add(new Note + { + Column = h.Column, + StartTime = h.EndTime, + Samples = h.GetNodeSamples((h.NodeSamples?.Count - 1) ?? 1) + }); + } + } + + maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType().Concat(newObjects).OrderBy(h => h.StartTime).ToList(); + } + + } + +} From 146c54a2c14608b95363647a0e965aeeb47aa03e Mon Sep 17 00:00:00 2001 From: Spooghetti420 Date: Thu, 27 Jan 2022 21:02:59 +0000 Subject: [PATCH 222/587] Fix code formatting --- .../Mods/TestSceneManiaModNoHolds.cs | 1 - osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.cs index 411f891f43..f22b724c99 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.cs @@ -28,7 +28,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods Assert.That(!testBeatmap.HitObjects.OfType().Any()); } - private static IBeatmap createBeatmap() { var beatmap = createRawBeatmap(); diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs index 0d5ca44898..a18dbad4a1 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs @@ -69,7 +69,5 @@ namespace osu.Game.Rulesets.Mania.Mods maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType().Concat(newObjects).OrderBy(h => h.StartTime).ToList(); } - } - -} +} \ No newline at end of file From f59828e2d9dc745e352ecbbf01fe811389e2ed47 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 28 Jan 2022 13:27:12 +0900 Subject: [PATCH 223/587] Add audio feedback to song select 'random' --- osu.Game/Screens/Select/BeatmapCarousel.cs | 29 +++++++++++++++++++++- osu.Game/Screens/Select/SongSelect.cs | 21 +++++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c27915c383..ef19eb187a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; @@ -151,6 +153,10 @@ namespace osu.Game.Screens.Select private readonly DrawablePool setPool = new DrawablePool(100); + private Sample spinSample; + + private int visibleSetsCount; + public BeatmapCarousel() { root = new CarouselRoot(this); @@ -169,8 +175,10 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, AudioManager audio) { + spinSample = audio.Samples.Get("SongSelect/random-spin"); + config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); @@ -419,6 +427,9 @@ namespace osu.Game.Screens.Select return false; var visibleSets = beatmapSets.Where(s => !s.Filtered.Value).ToList(); + + visibleSetsCount = visibleSets.Count; + if (!visibleSets.Any()) return false; @@ -450,6 +461,9 @@ namespace osu.Game.Screens.Select else set = visibleSets.ElementAt(RNG.Next(visibleSets.Count)); + if (selectedBeatmapSet != null) + playSpinSample(distanceBetween(set, selectedBeatmapSet)); + select(set); return true; } @@ -464,12 +478,25 @@ namespace osu.Game.Screens.Select { if (RandomAlgorithm.Value == RandomSelectAlgorithm.RandomPermutation) previouslyVisitedRandomSets.Remove(selectedBeatmapSet); + + if (selectedBeatmapSet != null) + playSpinSample(distanceBetween(beatmap, selectedBeatmapSet)); + select(beatmap); break; } } } + private double distanceBetween(CarouselItem item1, CarouselItem item2) => Math.Ceiling(Math.Abs(item1.CarouselYPosition - item2.CarouselYPosition) / DrawableCarouselItem.MAX_HEIGHT); + + private void playSpinSample(double distance) + { + var chan = spinSample.GetChannel(); + chan.Frequency.Value = 1f + Math.Min(1f, distance / visibleSetsCount); + chan.Play(); + } + private void select(CarouselItem item) { if (!AllowSelection) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 10150fcd9f..6fbadfe6cf 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -100,6 +100,7 @@ namespace osu.Game.Screens.Select private Sample sampleChangeDifficulty; private Sample sampleChangeBeatmap; + private Sample sampleRandomBeatmap; private Container carouselContainer; @@ -109,6 +110,8 @@ namespace osu.Game.Screens.Select private double audioFeedbackLastPlaybackTime; + private bool randomClicked; + [Resolved] private MusicController music { get; set; } @@ -288,6 +291,7 @@ namespace osu.Game.Screens.Select sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); sampleChangeBeatmap = audio.Samples.Get(@"SongSelect/select-expand"); + sampleRandomBeatmap = audio.Samples.Get(@"SongSelect/select-random"); SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection"); if (dialogOverlay != null) @@ -315,8 +319,16 @@ namespace osu.Game.Screens.Select (new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom { - NextRandom = () => Carousel.SelectNextRandom(), - PreviousRandom = Carousel.SelectPreviousRandom + NextRandom = () => + { + randomClicked = true; + Carousel.SelectNextRandom(); + }, + PreviousRandom = () => + { + randomClicked = true; + Carousel.SelectPreviousRandom(); + } }, null), (new FooterButtonOptions(), BeatmapOptions) }; @@ -486,7 +498,9 @@ namespace osu.Game.Screens.Select { if (beatmap != null && beatmapInfoPrevious != null && Time.Current - audioFeedbackLastPlaybackTime >= 50) { - if (beatmap.BeatmapSet?.ID == beatmapInfoPrevious.BeatmapSet?.ID) + if (randomClicked) + sampleRandomBeatmap.Play(); + else if (beatmap.BeatmapSet?.ID == beatmapInfoPrevious.BeatmapSet?.ID) sampleChangeDifficulty.Play(); else sampleChangeBeatmap.Play(); @@ -494,6 +508,7 @@ namespace osu.Game.Screens.Select audioFeedbackLastPlaybackTime = Time.Current; } + randomClicked = false; beatmapInfoPrevious = beatmap; } From c44af4853d6aabb82006d7944e146d82c06401fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 13:43:53 +0900 Subject: [PATCH 224/587] Add thread safety to `PollingComponent.Poll` implementations --- .../Components/ListingPollingComponent.cs | 14 ++++++++------ .../Components/SelectionPollingComponent.cs | 15 +++++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index fcf7767958..666d425f62 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Components }); } - private GetRoomsRequest pollReq; + private GetRoomsRequest lastPollRequest; protected override Task Poll() { @@ -45,10 +45,11 @@ namespace osu.Game.Screens.OnlinePlay.Components var tcs = new TaskCompletionSource(); - pollReq?.Cancel(); - pollReq = new GetRoomsRequest(Filter.Value.Status, Filter.Value.Category); + lastPollRequest?.Cancel(); - pollReq.Success += result => + var req = new GetRoomsRequest(Filter.Value.Status, Filter.Value.Category); + + req.Success += result => { foreach (var existing in RoomManager.Rooms.ToArray()) { @@ -66,10 +67,11 @@ namespace osu.Game.Screens.OnlinePlay.Components tcs.SetResult(true); }; - pollReq.Failure += _ => tcs.SetResult(false); + req.Failure += _ => tcs.SetResult(false); - API.Queue(pollReq); + API.Queue(req); + lastPollRequest = req; return tcs.Task; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index 22842fbb9e..e05bdf8c8e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.OnlinePlay.Components this.room = room; } - private GetRoomRequest pollReq; + private GetRoomRequest lastPollRequest; protected override Task Poll() { @@ -30,19 +30,22 @@ namespace osu.Game.Screens.OnlinePlay.Components var tcs = new TaskCompletionSource(); - pollReq?.Cancel(); - pollReq = new GetRoomRequest(room.RoomID.Value.Value); + lastPollRequest?.Cancel(); - pollReq.Success += result => + var req = new GetRoomRequest(room.RoomID.Value.Value); + + req.Success += result => { result.RemoveExpiredPlaylistItems(); RoomManager.AddOrUpdateRoom(result); tcs.SetResult(true); }; - pollReq.Failure += _ => tcs.SetResult(false); + req.Failure += _ => tcs.SetResult(false); - API.Queue(pollReq); + API.Queue(req); + + lastPollRequest = req; return tcs.Task; } From c953a5d5034416bb32ee2caada7c0a74f0b4551a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 13:44:09 +0900 Subject: [PATCH 225/587] Ensure `PollingComponent.Poll` is always called from the update thread Not strictly required since all `Poll` implementations are now threadsafe, but extra safety is never a bad thing? --- osu.Game/Online/PollingComponent.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index 243be8da44..5eddb3b49d 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; @@ -66,6 +68,8 @@ namespace osu.Game.Online private void doPoll() { + Debug.Assert(ThreadSafety.IsUpdateThread); + scheduledPoll = null; pollingActive = true; Poll().ContinueWith(_ => pollComplete()); @@ -96,13 +100,13 @@ namespace osu.Game.Online if (!lastTimePolled.HasValue) { - doPoll(); + Scheduler.AddOnce(doPoll); return; } if (Time.Current - lastTimePolled.Value > TimeBetweenPolls.Value) { - doPoll(); + Scheduler.AddOnce(doPoll); return; } From 91be77ad3d61f844c997681cfdf5b2643115bd43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 14:01:10 +0900 Subject: [PATCH 226/587] Fix null ref in `ComposeScreen` when ruleset doesn't provide a composer --- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 2bdf59b21c..2cde962b12 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -82,6 +82,11 @@ namespace osu.Game.Screens.Edit.Compose protected override void LoadComplete() { base.LoadComplete(); + + // May be null in the case of a ruleset that doesn't have editor support, see CreateMainContent(). + if (composer == null) + return; + EditorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) => updateClipboardActionAvailability()); clipboard.BindValueChanged(_ => updateClipboardActionAvailability()); composer.OnLoadComplete += _ => updateClipboardActionAvailability(); From b3856c900541e1af68aa8e4a5c55b9a03f62356f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 14:01:31 +0900 Subject: [PATCH 227/587] Fix editor crashing on custom rulesets due to `ChangeHandler` not being supported As per https://github.com/ppy/osu/discussions/16668, even without proper saving support some ruleset developers do want to work on the editor. This brings things back into a workable state. --- osu.Game/Screens/Edit/Editor.cs | 46 +++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 4812da98f4..2fead84deb 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -28,6 +28,8 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; @@ -61,7 +63,16 @@ namespace osu.Game.Screens.Edit public override bool? AllowTrackAdjustments => false; - protected bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash; + protected bool HasUnsavedChanges + { + get + { + if (!canSave) + return false; + + return lastSavedHash != changeHandler?.CurrentStateHash; + } + } [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -72,10 +83,15 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private DialogOverlay dialogOverlay { get; set; } + [Resolved(canBeNull: true)] + private NotificationOverlay notifications { get; set; } + public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; private readonly Bindable samplePlaybackDisabled = new Bindable(); + private bool canSave; + private bool exitConfirmed; private string lastSavedHash; @@ -92,6 +108,8 @@ namespace osu.Game.Screens.Edit private IBeatmap playableBeatmap; private EditorBeatmap editorBeatmap; + + [CanBeNull] // Should be non-null once it can support custom rulesets. private EditorChangeHandler changeHandler; private EditorMenuBar menuBar; @@ -172,8 +190,14 @@ namespace osu.Game.Screens.Edit AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin(), loadableBeatmap.BeatmapInfo)); dependencies.CacheAs(editorBeatmap); - changeHandler = new EditorChangeHandler(editorBeatmap); - dependencies.CacheAs(changeHandler); + + canSave = editorBeatmap.BeatmapInfo.Ruleset.CreateInstance() is ILegacyRuleset; + + if (canSave) + { + changeHandler = new EditorChangeHandler(editorBeatmap); + dependencies.CacheAs(changeHandler); + } beatDivisor.Value = editorBeatmap.BeatmapInfo.BeatDivisor; beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); @@ -311,8 +335,8 @@ namespace osu.Game.Screens.Edit } }); - changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); - changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); + changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); + changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); menuBar.Mode.ValueChanged += onModeChanged; } @@ -353,6 +377,12 @@ namespace osu.Game.Screens.Edit protected void Save() { + if (!canSave) + { + notifications?.Post(new SimpleErrorNotification { Text = "Saving is not supported for this ruleset yet, sorry!" }); + return; + } + // no longer new after first user-triggered save. isNewBeatmap = false; @@ -648,9 +678,9 @@ namespace osu.Game.Screens.Edit #endregion - protected void Undo() => changeHandler.RestoreState(-1); + protected void Undo() => changeHandler?.RestoreState(-1); - protected void Redo() => changeHandler.RestoreState(1); + protected void Redo() => changeHandler?.RestoreState(1); private void resetTrack(bool seekToStart = false) { @@ -761,7 +791,7 @@ namespace osu.Game.Screens.Edit private void updateLastSavedHash() { - lastSavedHash = changeHandler.CurrentStateHash; + lastSavedHash = changeHandler?.CurrentStateHash; } private List createFileMenuItems() From b7d8c9bf067f64021a4648ecfd413648258f373f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 14:18:10 +0900 Subject: [PATCH 228/587] Fix a couple of cases of incorrect equality checks in the case both values are null --- .../BeatmapSet/Scores/TopScoreStatisticsSection.cs | 3 +++ .../Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 1f3f73a60a..ec795cf6b2 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -107,6 +107,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { set { + if (score == null && value == null) + return; + if (score?.Equals(value) == true) return; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 2070e53257..31cbe91f5c 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -38,6 +38,14 @@ namespace osu.Game.Screens.Select.Leaderboards get => beatmapInfo; set { + if (beatmapInfo == null && value == null) + { + // always null scores to ensure a correct initial display. + // see weird `scoresLoadedOnce` logic in base implementation. + Scores = null; + return; + } + if (beatmapInfo?.Equals(value) == true) return; From f32d56e213fa26566b38ca12bb44cf9d8f0f0100 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 14:48:17 +0900 Subject: [PATCH 229/587] Bring `HoldForMenuButton` tests up-to-date in code quality --- .../Gameplay/TestSceneHoldForMenuButton.cs | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs index 235842acc9..e0765ab5fb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs @@ -3,9 +3,10 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Game.Graphics.Containers; using osu.Game.Screens.Play.HUD; using osuTK; using osuTK.Input; @@ -19,28 +20,35 @@ namespace osu.Game.Tests.Visual.Gameplay protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms - [BackgroundDependencyLoader] - private void load() + private HoldForMenuButton holdForMenuButton; + + [SetUpSteps] + public void SetUpSteps() { - HoldForMenuButton holdForMenuButton; - - Add(holdForMenuButton = new HoldForMenuButton + AddStep("create button", () => { - Origin = Anchor.BottomRight, - Anchor = Anchor.BottomRight, - Action = () => exitAction = true + exitAction = false; + + Child = holdForMenuButton = new HoldForMenuButton + { + Scale = new Vector2(2), + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Action = () => exitAction = true + }; }); + } - var text = holdForMenuButton.Children.OfType().First(); - + [Test] + public void TestMovementAndTrigger() + { AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(holdForMenuButton)); - AddUntilStep("Text visible", () => text.IsPresent && !exitAction); + AddUntilStep("Text visible", () => getSpriteText().IsPresent && !exitAction); AddStep("Trigger text fade out", () => InputManager.MoveMouseTo(Vector2.One)); - AddUntilStep("Text is not visible", () => !text.IsPresent && !exitAction); + AddUntilStep("Text is not visible", () => !getSpriteText().IsPresent && !exitAction); AddStep("Trigger exit action", () => { - exitAction = false; InputManager.MoveMouseTo(holdForMenuButton); InputManager.PressButton(MouseButton.Left); }); @@ -50,6 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Trigger exit action", () => InputManager.PressButton(MouseButton.Left)); AddUntilStep($"{nameof(holdForMenuButton.Action)} was triggered", () => exitAction); + AddStep("Release", () => InputManager.ReleaseButton(MouseButton.Left)); } } } From 28c8e07e3f223aaaa15fe840fc8ee375c31dc48d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 14:48:34 +0900 Subject: [PATCH 230/587] Ensure hold for menu button fades out if the cursor is never moved Closes https://github.com/ppy/osu/discussions/16669. --- .../Visual/Gameplay/TestSceneHoldForMenuButton.cs | 10 ++++++++++ osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs index e0765ab5fb..feec06f372 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs @@ -60,5 +60,15 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep($"{nameof(holdForMenuButton.Action)} was triggered", () => exitAction); AddStep("Release", () => InputManager.ReleaseButton(MouseButton.Left)); } + + [Test] + public void TestFadeOnNoInput() + { + AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.One)); + AddUntilStep("wait for text fade out", () => !getSpriteText().IsPresent); + AddUntilStep("wait for button fade out", () => holdForMenuButton.Alpha < 0.1f); + } + + private SpriteText getSpriteText() => holdForMenuButton.Children.OfType().First(); } } diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 5a7ef786d3..430f001427 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); } - private float positionalAdjust; + private float positionalAdjust = 1; // Start at 1 to handle the case where a user never send positional input. protected override bool OnMouseMove(MouseMoveEvent e) { From 32f9299fe082f7347aebfceef7aa6f8dd017f2f6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Jan 2022 15:26:29 +0900 Subject: [PATCH 231/587] Remove unused using --- osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs index feec06f372..ddb0872541 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; -using osu.Game.Graphics.Containers; using osu.Game.Screens.Play.HUD; using osuTK; using osuTK.Input; From cb7ae413feea2e53fd4b5aa3b59167ae0a398c13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 15:50:13 +0900 Subject: [PATCH 232/587] Ensure test game is always active --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index ebbd9bb5dd..dabf49c15e 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -158,6 +158,14 @@ namespace osu.Game.Tests.Visual Dependencies.Get().SetValue(Static.MutedAudioNotificationShownOnce, true); } + + protected override void Update() + { + base.Update(); + + // when running in visual tests and the window loses focus, we generally don't want the game to pause. + ((Bindable)IsActive).Value = true; + } } public class TestLoader : Loader From 778eebc94df6b2ff8a068b014aaee05de8bd594b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 15:50:22 +0900 Subject: [PATCH 233/587] Add test coverage of local score import and deletion --- .../Navigation/TestSceneScreenNavigation.cs | 90 +++++++++++++++---- osu.Game/Tests/Visual/OsuGameTestScene.cs | 3 + 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 89dca77af4..6d89feef52 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -10,16 +11,19 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; using osuTK; @@ -96,35 +100,52 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestRetryFromResults() { - Player player = null; - ResultsScreen results = null; + var getOriginalPlayer = playToResults(); - IWorkingBeatmap beatmap() => Game.Beatmap.Value; + AddStep("attempt to retry", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().First().Action()); + AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); + } - Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + [Test] + public void TestDeleteScoreAfterPlaying() + { + playToResults(); - AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + ScoreInfo score = null; + LeaderboardScore scorePanel = null; - AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score); - AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModDoubleTime { SpeedChange = { Value = 2 } } }); + AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == false)); - AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddStep("press back button", () => Game.ChildrenOfType().First().Action()); - AddUntilStep("wait for player", () => + AddStep("show local scores", () => Game.ChildrenOfType().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local)); + + AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType().FirstOrDefault(s => s.Score.Equals(score))) != null); + + AddStep("right click panel", () => { - // dismiss any notifications that may appear (ie. muted notification). - clickMouseInCentre(); - return (player = Game.ScreenStack.CurrentScreen as Player) != null; + InputManager.MoveMouseTo(scorePanel); + InputManager.Click(MouseButton.Right); }); - AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning); - AddStep("seek to near end", () => player.ChildrenOfType().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000)); - AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded); - AddStep("attempt to retry", () => results.ChildrenOfType().First().Action()); - AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player); + AddStep("click delete", () => + { + var dropdownItem = Game + .ChildrenOfType().First() + .ChildrenOfType().First() + .ChildrenOfType().First(i => i.Item.Text.ToString() == "Delete"); + + InputManager.MoveMouseTo(dropdownItem); + InputManager.Click(MouseButton.Left); + }); + + AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); + + AddAssert("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == true)); + + AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null); } [TestCase(true)] @@ -432,6 +453,37 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("test dispose doesn't crash", () => Game.Dispose()); } + private Func playToResults() + { + Player player = null; + + IWorkingBeatmap beatmap() => Game.Beatmap.Value; + + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModDoubleTime { SpeedChange = { Value = 2 } } }); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + // dismiss any notifications that may appear (ie. muted notification). + clickMouseInCentre(); + return (player = Game.ScreenStack.CurrentScreen as Player) != null; + }); + + AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning); + AddStep("seek to near end", () => player.ChildrenOfType().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000)); + AddUntilStep("wait for pass", () => (Game.ScreenStack.CurrentScreen as ResultsScreen)?.IsLoaded == true); + return () => player; + } + private void clickMouseInCentre() { InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre); diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index dabf49c15e..3b8d9a4cd1 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -14,6 +14,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays; @@ -111,6 +112,8 @@ namespace osu.Game.Tests.Visual public new ScreenStack ScreenStack => base.ScreenStack; + public RealmAccess Realm => Dependencies.Get(); + public new BackButton BackButton => base.BackButton; public new BeatmapManager BeatmapManager => base.BeatmapManager; From 2453bf5ed0b8b3660c2a109ad2720ba125b1fc2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 15:54:51 +0900 Subject: [PATCH 234/587] Add test coverage of the same thing but via "clear all scores" button --- .../Navigation/TestSceneScreenNavigation.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 6d89feef52..3be7cf7c5c 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -106,6 +106,35 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); } + [Test] + public void TestDeleteAllScoresAfterPlaying() + { + playToResults(); + + ScoreInfo score = null; + LeaderboardScore scorePanel = null; + + AddStep("get score", () => score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score); + + AddAssert("ensure score is databased", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == false)); + + AddStep("press back button", () => Game.ChildrenOfType().First().Action()); + + AddStep("show local scores", () => Game.ChildrenOfType().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local)); + + AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType().FirstOrDefault(s => s.Score.Equals(score))) != null); + + AddStep("open options", () => InputManager.Key(Key.F3)); + + AddStep("choose clear all scores", () => InputManager.Key(Key.Number4)); + + AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); + + AddAssert("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == true)); + + AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null); + } + [Test] public void TestDeleteScoreAfterPlaying() { From 0d3ac4fd9c72db8eb2049d07652375c0de419f0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 15:08:39 +0900 Subject: [PATCH 235/587] Fix delete local scores crashing the game --- osu.Game/Scoring/ScoreManager.cs | 9 +++++++++ osu.Game/Screens/Select/BeatmapClearScoresDialog.cs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 8f665224ee..6f9cce2d3c 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -266,6 +266,15 @@ namespace osu.Game.Scoring }); } + public void Delete(BeatmapInfo beatmap, bool silent = false) + { + realm.Run(r => + { + var beatmapScores = r.Find(beatmap.ID).Scores.ToList(); + scoreModelManager.Delete(beatmapScores, silent); + }); + } + public void Delete(List items, bool silent = false) { scoreModelManager.Delete(items, silent); diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index 774d3b4b28..4a16be4a3a 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Select Text = @"Yes. Please.", Action = () => { - Task.Run(() => scoreManager.Delete(s => !s.DeletePending && s.BeatmapInfo.ID == beatmapInfo.ID)) + Task.Run(() => scoreManager.Delete(beatmapInfo)) .ContinueWith(_ => onCompletion); } }, From 4d9b61212b22d154c0632c0d50978e90ecc59f9d Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 28 Jan 2022 18:13:51 +0900 Subject: [PATCH 236/587] Add 'cursor tap' audio feedback --- osu.Game/Graphics/Cursor/MenuCursor.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 8e272f637f..a89d8dac71 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using System; using JetBrains.Annotations; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; @@ -30,13 +32,17 @@ namespace osu.Game.Graphics.Cursor private DragRotationState dragRotationState; private Vector2 positionMouseDown; + private Sample tapSample; + [BackgroundDependencyLoader(true)] - private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager) + private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager, AudioManager audio) { cursorRotate = config.GetBindable(OsuSetting.CursorRotation); if (screenshotManager != null) screenshotCursorVisibility.BindTo(screenshotManager.CursorVisibility); + + tapSample = audio.Samples.Get(@"UI/cursor-tap"); } protected override bool OnMouseMove(MouseMoveEvent e) @@ -70,6 +76,18 @@ namespace osu.Game.Graphics.Cursor return base.OnMouseMove(e); } + protected override bool OnClick(ClickEvent e) + { + var channel = tapSample.GetChannel(); + + // scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird) + channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75; + channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02); + channel.Play(); + + return base.OnClick(e); + } + protected override bool OnMouseDown(MouseDownEvent e) { if (State.Value == Visibility.Visible) From 17f0d7897b8a361a7a1fc5e6faa2379efc0a9a8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 18:35:59 +0900 Subject: [PATCH 237/587] Increase lenience of alpha check in `TestSceneOsuModNoScope` I believe the [test failures](https://github.com/ppy/osu/runs/4977283066?check_suite_focus=true) we're seeing here are due to the implementation of interpolation of the alpha being frame dependent (in a way that doesn't interact well with tests). The reason for never hitting the expected value is that the beatmap ends, causing the cursor to become fully visible again. It's probably already good-enough for most cases, so let's attempt to silence these test failures by not checking so precisely for the alpha value. We're checking for either 1 or 0 so it's not too important how close it is to either. --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs index 8e226c7ded..44404ca245 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs @@ -145,6 +145,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods private bool isBreak() => Player.IsBreakTime.Value; - private bool cursorAlphaAlmostEquals(float alpha) => Precision.AlmostEquals(Player.DrawableRuleset.Cursor.Alpha, alpha); + private bool cursorAlphaAlmostEquals(float alpha) => Precision.AlmostEquals(Player.DrawableRuleset.Cursor.Alpha, alpha, 0.1f); } } From 142a67e1634fb379e8725d3ad51e5cf4dd9825a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 18:53:28 +0900 Subject: [PATCH 238/587] Fix approach rate not being transferred from OD on older beatmaps --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 01ba0fcc9f..07ada8ecc4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -311,10 +311,13 @@ namespace osu.Game.Beatmaps.Formats case @"OverallDifficulty": difficulty.OverallDifficulty = Parsing.ParseFloat(pair.Value); + if (!hasApproachRate) + difficulty.ApproachRate = difficulty.OverallDifficulty; break; case @"ApproachRate": difficulty.ApproachRate = Parsing.ParseFloat(pair.Value); + hasApproachRate = true; break; case @"SliderMultiplier": @@ -432,6 +435,7 @@ namespace osu.Game.Beatmaps.Formats private readonly List pendingControlPoints = new List(); private readonly HashSet pendingControlPointTypes = new HashSet(); private double pendingControlPointsTime; + private bool hasApproachRate; private void addControlPoint(double time, ControlPoint point, bool timingChange) { From 53ca597e2b2c85431c722f1801f00805179b150b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 19:12:32 +0900 Subject: [PATCH 239/587] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index f85a96f819..2902c74f0a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index aa6fb93aa0..b74d51e8f1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index fbb4688588..90ffb9605f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From bda3cdc9a773ac76c36470174e13db398e33fd7e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Jan 2022 19:28:42 +0900 Subject: [PATCH 240/587] Add tests --- .../Formats/LegacyBeatmapDecoderTest.cs | 42 +++++++++++++++++++ ...approach-rate-after-overall-difficulty.osu | 3 ++ ...pproach-rate-before-overall-difficulty.osu | 3 ++ .../Resources/undefined-approach-rate.osu | 2 + 4 files changed, 50 insertions(+) create mode 100644 osu.Game.Tests/Resources/approach-rate-after-overall-difficulty.osu create mode 100644 osu.Game.Tests/Resources/approach-rate-before-overall-difficulty.osu create mode 100644 osu.Game.Tests/Resources/undefined-approach-rate.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 3a6051ed43..468cb7683c 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -821,5 +821,47 @@ namespace osu.Game.Tests.Beatmaps.Formats }); } } + + [Test] + public void TestUndefinedApproachRateInheritsOverallDifficulty() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("undefined-approach-rate.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var decoded = decoder.Decode(stream); + Assert.That(decoded.Difficulty.ApproachRate, Is.EqualTo(1)); + Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1)); + } + } + + [Test] + public void TestApproachRateDefinedBeforeOverallDifficulty() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("approach-rate-before-overall-difficulty.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var decoded = decoder.Decode(stream); + Assert.That(decoded.Difficulty.ApproachRate, Is.EqualTo(9)); + Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1)); + } + } + + [Test] + public void TestApproachRateDefinedAfterOverallDifficulty() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("approach-rate-after-overall-difficulty.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var decoded = decoder.Decode(stream); + Assert.That(decoded.Difficulty.ApproachRate, Is.EqualTo(9)); + Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1)); + } + } } } diff --git a/osu.Game.Tests/Resources/approach-rate-after-overall-difficulty.osu b/osu.Game.Tests/Resources/approach-rate-after-overall-difficulty.osu new file mode 100644 index 0000000000..23732aef8c --- /dev/null +++ b/osu.Game.Tests/Resources/approach-rate-after-overall-difficulty.osu @@ -0,0 +1,3 @@ +[Difficulty] +OverallDifficulty:1 +ApproachRate:9 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/approach-rate-before-overall-difficulty.osu b/osu.Game.Tests/Resources/approach-rate-before-overall-difficulty.osu new file mode 100644 index 0000000000..18885c6624 --- /dev/null +++ b/osu.Game.Tests/Resources/approach-rate-before-overall-difficulty.osu @@ -0,0 +1,3 @@ +[Difficulty] +ApproachRate:9 +OverallDifficulty:1 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/undefined-approach-rate.osu b/osu.Game.Tests/Resources/undefined-approach-rate.osu new file mode 100644 index 0000000000..0de24238bf --- /dev/null +++ b/osu.Game.Tests/Resources/undefined-approach-rate.osu @@ -0,0 +1,2 @@ +[Difficulty] +OverallDifficulty:1 \ No newline at end of file From 397971c6313eff22d645c07d782547a4e7683a2e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Jan 2022 22:06:34 +0900 Subject: [PATCH 241/587] Change FrameDataBundle.Frames into an IList --- osu.Game/Online/Spectator/FrameDataBundle.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs index 0e59cdf4ce..a4c4972989 100644 --- a/osu.Game/Online/Spectator/FrameDataBundle.cs +++ b/osu.Game/Online/Spectator/FrameDataBundle.cs @@ -20,16 +20,16 @@ namespace osu.Game.Online.Spectator public FrameHeader Header { get; set; } [Key(1)] - public IEnumerable Frames { get; set; } + public IList Frames { get; set; } - public FrameDataBundle(ScoreInfo score, IEnumerable frames) + public FrameDataBundle(ScoreInfo score, IList frames) { Frames = frames; Header = new FrameHeader(score); } [JsonConstructor] - public FrameDataBundle(FrameHeader header, IEnumerable frames) + public FrameDataBundle(FrameHeader header, IList frames) { Header = header; Frames = frames; From 3037a3a7696b7f00cef3820c5c5994949de78622 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Jan 2022 22:26:05 +0900 Subject: [PATCH 242/587] Purge final spectator frames before ending play --- .../Visual/Gameplay/TestSceneSpectator.cs | 18 ++++++++++++++++++ osu.Game/Online/Spectator/SpectatorClient.cs | 10 ++++++++-- .../Visual/Spectator/TestSpectatorClient.cs | 11 +++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 0afd2c76dd..d1a25b07ec 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -15,11 +15,14 @@ using osu.Game.Online.Spectator; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; +using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Tests.Visual.Spectator; +using osuTK; namespace osu.Game.Tests.Visual.Gameplay { @@ -200,6 +203,21 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectator); } + [Test] + public void TestFinalFramesPurgedBeforeEndingPlay() + { + AddStep("begin playing", () => spectatorClient.BeginPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()), new Score())); + + AddStep("send frames and finish play", () => + { + spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero)); + spectatorClient.EndPlaying(); + }); + + // We can't access API because we're an "online" test. + AddAssert("last received frame has time = 1000", () => spectatorClient.LastReceivedFrame.First().Value.Time == 1000); + } + private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 4da9bace70..fddb94fad7 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -167,6 +167,9 @@ namespace osu.Game.Online.Spectator if (!IsPlaying) return; + if (pendingFrames.Count > 0) + purgePendingFrames(true); + IsPlaying = false; currentBeatmap = null; @@ -238,9 +241,12 @@ namespace osu.Game.Online.Spectator purgePendingFrames(); } - private void purgePendingFrames() + private void purgePendingFrames(bool force = false) { - if (lastSend?.IsCompleted == false) + if (lastSend?.IsCompleted == false && !force) + return; + + if (pendingFrames.Count == 0) return; var frames = pendingFrames.ToArray(); diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index f206d4f8b0..3f3d0f5b01 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -14,6 +14,7 @@ using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Replays; using osu.Game.Scoring; namespace osu.Game.Tests.Visual.Spectator @@ -27,12 +28,22 @@ namespace osu.Game.Tests.Visual.Spectator public override IBindable IsConnected { get; } = new Bindable(true); + public readonly Dictionary LastReceivedFrame = new Dictionary(); private readonly Dictionary userBeatmapDictionary = new Dictionary(); private readonly Dictionary userNextFrameDictionary = new Dictionary(); [Resolved] private IAPIProvider api { get; set; } = null!; + public TestSpectatorClient() + { + OnNewFrames += (i, bundle) => + { + if (PlayingUsers.Contains(i)) + LastReceivedFrame[i] = bundle.Frames[^1]; + }; + } + /// /// Starts play for an arbitrary user. /// From 1253e1ecc1f7ec68f78f7b30878b673b2c37345d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jan 2022 20:25:12 +0100 Subject: [PATCH 243/587] Replace LINQ `Count()` invocation with count property access --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 4790bd44db..90abdf2ba3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -184,7 +184,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void onNewFrames(int userId, FrameDataBundle frames) { - Logger.Log($"Received {frames.Frames.Count()} new frames ({string.Join(',', frames.Frames.Select(f => ((int)f.Time).ToString()))})"); + Logger.Log($"Received {frames.Frames.Count} new frames ({string.Join(',', frames.Frames.Select(f => ((int)f.Time).ToString()))})"); foreach (var legacyFrame in frames.Frames) { From a4aa501bb540cd83f988febd90d128f1c890b0b9 Mon Sep 17 00:00:00 2001 From: Spooghetti420 Date: Fri, 28 Jan 2022 21:59:53 +0000 Subject: [PATCH 244/587] Change threshold from ms to beat-based, add tests --- .../Mods/TestSceneManiaModHoldOff.cs | 130 ++++++++++++++++++ .../Mods/TestSceneManiaModNoHolds.cs | 51 ------- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- ...{ManiaModNoHolds.cs => ManiaModHoldOff.cs} | 34 +++-- .../Mods/ManiaModNoLongNotes.cs | 86 ------------ 5 files changed, 155 insertions(+), 148 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs delete mode 100644 osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.cs rename osu.Game.Rulesets.Mania/Mods/{ManiaModNoHolds.cs => ManiaModHoldOff.cs} (66%) delete mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs new file mode 100644 index 0000000000..e157cb50b1 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs @@ -0,0 +1,130 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Tests.Visual; +using System.Collections.Generic; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Difficulty; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public class TestSceneManiaModHoldOff : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Test] + public void TestMapHasNoHoldNotes() + { + var testBeatmap = createModdedBeatmap(); + Assert.False(testBeatmap.HitObjects.OfType().Any()); + } + + [Test] + public void TestCorrectNoteValues() + { + var testBeatmap = createRawBeatmap(); + var noteValues = new List(testBeatmap.HitObjects.OfType().Count()); + foreach (HoldNote h in testBeatmap.HitObjects.OfType()) { + noteValues.Add(ManiaModHoldOff.getNoteValue(h, (ManiaBeatmap)testBeatmap)); + } + noteValues.Sort(); + Assert.AreEqual(noteValues, new List { 0.125, 0.250, 0.500, 1.000, 2.000 }); + } + + [TestCase(ManiaModHoldOff.BeatDivisors.Whole)] + [TestCase(ManiaModHoldOff.BeatDivisors.Half)] + [TestCase(ManiaModHoldOff.BeatDivisors.Quarter)] + [TestCase(ManiaModHoldOff.BeatDivisors.Eighth)] + public void TestCorrectObjectCount(ManiaModHoldOff.BeatDivisors minBeatSnap) { + /* + This test is to ensure that, given that end notes are enabled, + the mod produces the expected number of objects when the mod is applied. + */ + + // Mod settings will be set to include the correct beat snap value + var rawBeatmap = createRawBeatmap(); + var testBeatmap = createModdedBeatmap(minBeatSnap); + + // Calculate expected number of objects + int expectedObjectCount = 0; + double beatSnapValue = 1/(Math.Pow(2, (int)minBeatSnap)); + + foreach (ManiaHitObject h in rawBeatmap.HitObjects) { + // Both notes and hold notes account for at least one object + expectedObjectCount++; + if (h.GetType() == typeof(HoldNote)) { + var noteValue = ManiaModHoldOff.getNoteValue((HoldNote)h, (ManiaBeatmap)rawBeatmap); + if (noteValue >= beatSnapValue) { + // Should generate an end note if it's longer than the minimum note value + expectedObjectCount++; + } + } + } + + Assert.That(testBeatmap.HitObjects.Count() == expectedObjectCount); + } + + [Test] + public void TestDifficultyIncrease() { + // A lower minimum beat snap divisor should only make the map harder, never easier + // (as more notes can be spawned) + var beatmaps = new ManiaBeatmap[] { + createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Whole), + createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Half), + createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Quarter), + createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Eighth), + createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Sixteenth) + }; + + var mapDifficulties = new double[beatmaps.Length]; + for (int i = 0; i < mapDifficulties.Length; i++) { + var workingBeatmap = new TestWorkingBeatmap(beatmaps[i]); + var difficultyCalculator = new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, workingBeatmap); + mapDifficulties[i] = difficultyCalculator.Calculate().StarRating; + if (i > 0) { + Assert.LessOrEqual(mapDifficulties[i-1], mapDifficulties[i]); + Assert.LessOrEqual(beatmaps[i-1].HitObjects.Count, beatmaps[i].HitObjects.Count); + } + } + } + + private static ManiaBeatmap createModdedBeatmap(ManiaModHoldOff.BeatDivisors minBeatSnap=ManiaModHoldOff.BeatDivisors.Whole) + { + var beatmap = createRawBeatmap(); + var holdOffMod = new ManiaModHoldOff(); + holdOffMod.MinBeatSnap.Value = minBeatSnap; // Set the specified beat snap setting + Assert.AreEqual(holdOffMod.MinBeatSnap.Value, minBeatSnap); + + foreach (var hitObject in beatmap.HitObjects) + hitObject.ApplyDefaults(beatmap.ControlPointInfo, new BeatmapDifficulty()); + + holdOffMod.ApplyToBeatmap(beatmap); + + return (ManiaBeatmap)beatmap; + } + private static ManiaBeatmap createRawBeatmap() + { + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }); + beatmap.ControlPointInfo.Add(0.0, new TimingControlPoint { BeatLength = 1000 } ); // Set BPM to 60 + + // Add test hit objects + beatmap.HitObjects.Add(new Note { StartTime = 4000 }); + beatmap.HitObjects.Add(new Note { StartTime = 4500 }); + beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 125 }); // 1/8 note + beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 250 }); // 1/4 note + beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 500 }); // 1/2 note + beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 1000 }); // 1/1 note + beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 2000 }); // 2/1 note + + return beatmap; + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.cs deleted file mode 100644 index f22b724c99..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoHolds.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mania.Mods; -using osu.Game.Tests.Visual; -using osu.Game.Rulesets.Mania.Tests; -using System.Collections.Generic; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Objects; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Mania.Beatmaps; - -namespace osu.Game.Rulesets.Mania.Tests.Mods -{ - public class TestSceneManiaModNoHolds : ModTestScene - { - protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); - - [Test] - public void TestMapHasNoHeldNotes() - { - var testBeatmap = createBeatmap(); - Assert.That(!testBeatmap.HitObjects.OfType().Any()); - } - - private static IBeatmap createBeatmap() - { - var beatmap = createRawBeatmap(); - var noHoldsMod = new ManiaModNoHolds(); - - foreach (var hitObject in beatmap.HitObjects) - hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - noHoldsMod.ApplyToBeatmap(beatmap); - - return beatmap; - } - private static IBeatmap createRawBeatmap() - { - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }); - beatmap.HitObjects.Add(new Note { StartTime = 1000 }); - beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, EndTime = 3000 }); - return beatmap; - } - } -} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 55ca1a465e..2098c7f5d8 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Mania new ManiaModClassic(), new ManiaModInvert(), new ManiaModConstantSpeed(), - new ManiaModNoHolds() + new ManiaModHoldOff() }; case ModType.Automation: diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs similarity index 66% rename from osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs rename to osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index a18dbad4a1..cfdb58ee67 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNoHolds.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -2,28 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; using osu.Framework.Graphics.Sprites; using System; using System.Collections.Generic; -using osu.Game.Audio; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Framework.Utils; -using osu.Game.Overlays.Settings; using osu.Framework.Bindables; using osu.Game.Configuration; -using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModNoHolds : Mod, IApplicableAfterBeatmapConversion + public class ManiaModHoldOff : Mod, IApplicableAfterBeatmapConversion { - public override string Name => "No Holds"; + public override string Name => "Hold Off"; - public override string Acronym => "NH"; + public override string Acronym => "HO"; public override double ScoreMultiplier => 1; @@ -40,11 +35,15 @@ namespace osu.Game.Rulesets.Mania.Mods Value = true }; + [SettingSource("Minimum end note beat snap", "Don't add end notes for hold notes shorter than this beat division")] + public Bindable MinBeatSnap { get; } = new Bindable(defaultValue: BeatDivisors.Half); + public void ApplyToBeatmap(IBeatmap beatmap) { var maniaBeatmap = (ManiaBeatmap)beatmap; var newObjects = new List(); + var beatSnap = 1/(Math.Pow(2, (double)MinBeatSnap.Value)); foreach (var h in beatmap.HitObjects.OfType()) { // Add a note for the beginning of the hold note @@ -56,7 +55,8 @@ namespace osu.Game.Rulesets.Mania.Mods }); // Don't add an end note if the duration is shorter than some threshold, or end notes are disabled - if (AddEndNotes.Value && h.Duration > 200) + var noteValue = getNoteValue(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc. + if (AddEndNotes.Value && noteValue >= beatSnap) { newObjects.Add(new Note { @@ -66,8 +66,22 @@ namespace osu.Game.Rulesets.Mania.Mods }); } } - maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType().Concat(newObjects).OrderBy(h => h.StartTime).ToList(); } + + public static double getNoteValue(HoldNote holdNote, ManiaBeatmap beatmap) { + var bpmAtNoteTime = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BPM; + var noteValue = (60*holdNote.Duration)/(1000*bpmAtNoteTime); + return noteValue; + } + + public enum BeatDivisors + { + Whole, + Half, + Quarter, + Eighth, + Sixteenth + } } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs deleted file mode 100644 index d10003b783..0000000000 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNoLongNotes.cs +++ /dev/null @@ -1,86 +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.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mods; -using osu.Framework.Graphics.Sprites; -using System; -using System.Collections.Generic; -using osu.Game.Audio; -using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Framework.Utils; -using osu.Game.Overlays.Settings; -using osu.Framework.Bindables; -using osu.Game.Configuration; -using osu.Game.Graphics; - - - -namespace osu.Game.Rulesets.Mania.Mods -{ - public class ManiaModNoLongNotes : Mod, IApplicableAfterBeatmapConversion - { - - public override string Name => "No Long Notes"; - - public override string Acronym => "NL"; - - public override double ScoreMultiplier => 1; - - public override string Description => @"Turns all held notes into tap notes. No coordination required."; - - public override IconUsage? Icon => FontAwesome.Solid.DotCircle; - - public override ModType Type => ModType.Conversion; - - [SettingSource("Add end notes", "Also add a note at the end of a held note")] - public BindableBool AddEndNotes { get; } = new BindableBool - { - Default = true, - Value = true - }; - - [SettingSource("Length threshold", "Only add an end note for held notes longer than this threshold (in milliseconds)")] - public BindableNumber Threshold { get; } = new BindableDouble - { - MinValue = 1.0, - MaxValue = 1990.0, - Default = 200.0, - Value = 200.0, - Precision = 1.0, - }; - - public void ApplyToBeatmap(IBeatmap beatmap) - { - var maniaBeatmap = (ManiaBeatmap)beatmap; - - var newObjects = new List(); - beatmap.HitObjects.OfType().ForEach(h => - { - // Add a note for the beginning of the hold note - newObjects.Add(new Note - { - Column = h.Column, - StartTime = h.StartTime, - Samples = h.Samples - }); - - // Don't add an end note if the duration is below the threshold, or end notes are disabled - if (AddEndNotes.Value && h.Duration > Threshold.Value) - { - newObjects.Add(new Note - { - Column = h.Column, - StartTime = h.EndTime, - Samples = h.Samples - }); - } - }); - - maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType().Concat(newObjects).OrderBy(h => h.StartTime).ToList(); - } - } -} From b2ebcabdd5c56fa734a6eaf8eacc8b8c5c3db9a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Jan 2022 13:27:19 +0900 Subject: [PATCH 245/587] Fix potential crash during stable install migration due to multiple configuration files Apparently this can be a thing on windows. Closes https://github.com/ppy/osu/discussions/16689. --- osu.Game/IO/StableStorage.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index f5a8c4dc9e..84b7da91fc 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -34,11 +34,17 @@ namespace osu.Game.IO private string locateSongsDirectory() { - string configFile = GetFiles(".", $"osu!.{Environment.UserName}.cfg").SingleOrDefault(); + var configurationFiles = GetFiles(".", $"osu!.{Environment.UserName}.cfg"); - if (configFile != null) + // GetFiles returns case insensitive results, so multiple files could exist. + // Prefer a case-correct match, but fallback to any available. + string usableConfigFile = + configurationFiles.FirstOrDefault(f => f.Contains(Environment.UserName, StringComparison.Ordinal)) + ?? configurationFiles.FirstOrDefault(); + + if (usableConfigFile != null) { - using (var stream = GetStream(configFile)) + using (var stream = GetStream(usableConfigFile)) using (var textReader = new StreamReader(stream)) { string line; From c75ffe9b0764effd0c5270ac6b0a0bcd02c6762a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Jan 2022 13:47:04 +0900 Subject: [PATCH 246/587] Apply code style changes --- osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index cfdb58ee67..aa447356ff 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -43,7 +43,8 @@ namespace osu.Game.Rulesets.Mania.Mods var maniaBeatmap = (ManiaBeatmap)beatmap; var newObjects = new List(); - var beatSnap = 1/(Math.Pow(2, (double)MinBeatSnap.Value)); + double beatSnap = 1 / (Math.Pow(2, (double)MinBeatSnap.Value)); + foreach (var h in beatmap.HitObjects.OfType()) { // Add a note for the beginning of the hold note @@ -55,7 +56,8 @@ namespace osu.Game.Rulesets.Mania.Mods }); // Don't add an end note if the duration is shorter than some threshold, or end notes are disabled - var noteValue = getNoteValue(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc. + double noteValue = getNoteValue(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc. + if (AddEndNotes.Value && noteValue >= beatSnap) { newObjects.Add(new Note @@ -66,13 +68,14 @@ namespace osu.Game.Rulesets.Mania.Mods }); } } + maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType().Concat(newObjects).OrderBy(h => h.StartTime).ToList(); } - public static double getNoteValue(HoldNote holdNote, ManiaBeatmap beatmap) { - var bpmAtNoteTime = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BPM; - var noteValue = (60*holdNote.Duration)/(1000*bpmAtNoteTime); - return noteValue; + private static double getNoteValue(HoldNote holdNote, ManiaBeatmap beatmap) + { + double bpmAtNoteTime = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BPM; + return (60 * holdNote.Duration) / (1000 * bpmAtNoteTime); } public enum BeatDivisors @@ -84,4 +87,4 @@ namespace osu.Game.Rulesets.Mania.Mods Sixteenth } } -} \ No newline at end of file +} From 035a84e75c4b84f655ad8fadd27b24677bd66ae2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Jan 2022 14:05:23 +0900 Subject: [PATCH 247/587] Rename function and make `public` again for test usage --- .../Mods/TestSceneManiaModHoldOff.cs | 54 ++++++++++++------- .../Mods/ManiaModHoldOff.cs | 4 +- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs index e157cb50b1..ddf09aab33 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs @@ -32,9 +32,12 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { var testBeatmap = createRawBeatmap(); var noteValues = new List(testBeatmap.HitObjects.OfType().Count()); - foreach (HoldNote h in testBeatmap.HitObjects.OfType()) { - noteValues.Add(ManiaModHoldOff.getNoteValue(h, (ManiaBeatmap)testBeatmap)); + + foreach (HoldNote h in testBeatmap.HitObjects.OfType()) + { + noteValues.Add(ManiaModHoldOff.GetNoteDurationInBeatLength(h, (ManiaBeatmap)testBeatmap)); } + noteValues.Sort(); Assert.AreEqual(noteValues, new List { 0.125, 0.250, 0.500, 1.000, 2.000 }); } @@ -43,7 +46,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods [TestCase(ManiaModHoldOff.BeatDivisors.Half)] [TestCase(ManiaModHoldOff.BeatDivisors.Quarter)] [TestCase(ManiaModHoldOff.BeatDivisors.Eighth)] - public void TestCorrectObjectCount(ManiaModHoldOff.BeatDivisors minBeatSnap) { + public void TestCorrectObjectCount(ManiaModHoldOff.BeatDivisors minBeatSnap) + { /* This test is to ensure that, given that end notes are enabled, the mod produces the expected number of objects when the mod is applied. @@ -55,14 +59,19 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods // Calculate expected number of objects int expectedObjectCount = 0; - double beatSnapValue = 1/(Math.Pow(2, (int)minBeatSnap)); + double beatSnapValue = 1 / (Math.Pow(2, (int)minBeatSnap)); - foreach (ManiaHitObject h in rawBeatmap.HitObjects) { + foreach (ManiaHitObject h in rawBeatmap.HitObjects) + { // Both notes and hold notes account for at least one object expectedObjectCount++; - if (h.GetType() == typeof(HoldNote)) { - var noteValue = ManiaModHoldOff.getNoteValue((HoldNote)h, (ManiaBeatmap)rawBeatmap); - if (noteValue >= beatSnapValue) { + + if (h.GetType() == typeof(HoldNote)) + { + var noteValue = ManiaModHoldOff.GetNoteDurationInBeatLength((HoldNote)h, (ManiaBeatmap)rawBeatmap); + + if (noteValue >= beatSnapValue) + { // Should generate an end note if it's longer than the minimum note value expectedObjectCount++; } @@ -73,10 +82,12 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods } [Test] - public void TestDifficultyIncrease() { + public void TestDifficultyIncrease() + { // A lower minimum beat snap divisor should only make the map harder, never easier // (as more notes can be spawned) - var beatmaps = new ManiaBeatmap[] { + var beatmaps = new ManiaBeatmap[] + { createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Whole), createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Half), createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Quarter), @@ -85,18 +96,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods }; var mapDifficulties = new double[beatmaps.Length]; - for (int i = 0; i < mapDifficulties.Length; i++) { + + for (int i = 0; i < mapDifficulties.Length; i++) + { var workingBeatmap = new TestWorkingBeatmap(beatmaps[i]); var difficultyCalculator = new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, workingBeatmap); mapDifficulties[i] = difficultyCalculator.Calculate().StarRating; - if (i > 0) { - Assert.LessOrEqual(mapDifficulties[i-1], mapDifficulties[i]); - Assert.LessOrEqual(beatmaps[i-1].HitObjects.Count, beatmaps[i].HitObjects.Count); + + if (i > 0) + { + Assert.LessOrEqual(mapDifficulties[i - 1], mapDifficulties[i]); + Assert.LessOrEqual(beatmaps[i - 1].HitObjects.Count, beatmaps[i].HitObjects.Count); } } } - private static ManiaBeatmap createModdedBeatmap(ManiaModHoldOff.BeatDivisors minBeatSnap=ManiaModHoldOff.BeatDivisors.Whole) + private static ManiaBeatmap createModdedBeatmap(ManiaModHoldOff.BeatDivisors minBeatSnap = ManiaModHoldOff.BeatDivisors.Whole) { var beatmap = createRawBeatmap(); var holdOffMod = new ManiaModHoldOff(); @@ -110,17 +125,18 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods return (ManiaBeatmap)beatmap; } + private static ManiaBeatmap createRawBeatmap() { var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }); - beatmap.ControlPointInfo.Add(0.0, new TimingControlPoint { BeatLength = 1000 } ); // Set BPM to 60 + beatmap.ControlPointInfo.Add(0.0, new TimingControlPoint { BeatLength = 1000 }); // Set BPM to 60 // Add test hit objects beatmap.HitObjects.Add(new Note { StartTime = 4000 }); beatmap.HitObjects.Add(new Note { StartTime = 4500 }); - beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 125 }); // 1/8 note - beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 250 }); // 1/4 note - beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 500 }); // 1/2 note + beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 125 }); // 1/8 note + beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 250 }); // 1/4 note + beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 500 }); // 1/2 note beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 1000 }); // 1/1 note beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 2000 }); // 2/1 note diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index aa447356ff..637142aed6 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Mods }); // Don't add an end note if the duration is shorter than some threshold, or end notes are disabled - double noteValue = getNoteValue(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc. + double noteValue = GetNoteDurationInBeatLength(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc. if (AddEndNotes.Value && noteValue >= beatSnap) { @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mania.Mods maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType().Concat(newObjects).OrderBy(h => h.StartTime).ToList(); } - private static double getNoteValue(HoldNote holdNote, ManiaBeatmap beatmap) + public static double GetNoteDurationInBeatLength(HoldNote holdNote, ManiaBeatmap beatmap) { double bpmAtNoteTime = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BPM; return (60 * holdNote.Duration) / (1000 * bpmAtNoteTime); From c7580a5177eb24aad1a667bee76e098dcb60f559 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Jan 2022 14:06:43 +0900 Subject: [PATCH 248/587] Fix inspections in test scene --- .../Mods/TestSceneManiaModHoldOff.cs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs index ddf09aab33..e74f63abb3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods foreach (HoldNote h in testBeatmap.HitObjects.OfType()) { - noteValues.Add(ManiaModHoldOff.GetNoteDurationInBeatLength(h, (ManiaBeatmap)testBeatmap)); + noteValues.Add(ManiaModHoldOff.GetNoteDurationInBeatLength(h, testBeatmap)); } noteValues.Sort(); @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods if (h.GetType() == typeof(HoldNote)) { - var noteValue = ManiaModHoldOff.GetNoteDurationInBeatLength((HoldNote)h, (ManiaBeatmap)rawBeatmap); + double noteValue = ManiaModHoldOff.GetNoteDurationInBeatLength((HoldNote)h, rawBeatmap); if (noteValue >= beatSnapValue) { @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods } } - Assert.That(testBeatmap.HitObjects.Count() == expectedObjectCount); + Assert.That(testBeatmap.HitObjects.Count == expectedObjectCount); } [Test] @@ -86,16 +86,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { // A lower minimum beat snap divisor should only make the map harder, never easier // (as more notes can be spawned) - var beatmaps = new ManiaBeatmap[] + var beatmaps = new[] { - createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Whole), + createModdedBeatmap(), createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Half), createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Quarter), createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Eighth), createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Sixteenth) }; - var mapDifficulties = new double[beatmaps.Length]; + double[] mapDifficulties = new double[beatmaps.Length]; for (int i = 0; i < mapDifficulties.Length; i++) { @@ -114,8 +114,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods private static ManiaBeatmap createModdedBeatmap(ManiaModHoldOff.BeatDivisors minBeatSnap = ManiaModHoldOff.BeatDivisors.Whole) { var beatmap = createRawBeatmap(); - var holdOffMod = new ManiaModHoldOff(); - holdOffMod.MinBeatSnap.Value = minBeatSnap; // Set the specified beat snap setting + var holdOffMod = new ManiaModHoldOff + { + MinBeatSnap = { Value = minBeatSnap } + }; Assert.AreEqual(holdOffMod.MinBeatSnap.Value, minBeatSnap); foreach (var hitObject in beatmap.HitObjects) @@ -123,7 +125,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods holdOffMod.ApplyToBeatmap(beatmap); - return (ManiaBeatmap)beatmap; + return beatmap; } private static ManiaBeatmap createRawBeatmap() From 4c97ed676fc335a8640bcf1e39085e656b4e997f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Jan 2022 14:46:24 +0900 Subject: [PATCH 249/587] Fix score presentation tests not correctly entering song select before running --- osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 7656bf79dc..6c32171b29 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -16,6 +16,7 @@ using osu.Game.Screens; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Select; namespace osu.Game.Tests.Visual.Navigation { @@ -92,6 +93,9 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelect([Values] ScorePresentType type) { + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + var firstImport = importScore(1); presentAndConfirm(firstImport, type); @@ -102,6 +106,9 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelectDifferentRuleset([Values] ScorePresentType type) { + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + var firstImport = importScore(1); presentAndConfirm(firstImport, type); From e7823982d827b3371f80b4f74a5c567933c62551 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Jan 2022 18:38:45 +0900 Subject: [PATCH 250/587] Fix ruleset value not being transferred when `FinaliseSelection` is not called --- osu.Game/Screens/Select/SongSelect.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 10150fcd9f..f5b11448f8 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -619,6 +619,10 @@ namespace osu.Game.Screens.Select public override void OnSuspending(IScreen next) { + // Handle the case where FinaliseSelection is never called (ie. when a screen is pushed externally). + // Without this, it's possible for a transfer to happen while we are not the current screen. + transferRulesetValue(); + ModSelect.SelectedMods.UnbindFrom(selectedMods); ModSelect.Hide(); From aa582fb0e1a15de97b48203a7bcaed3c84a4d6c7 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Sat, 29 Jan 2022 20:38:12 +0800 Subject: [PATCH 251/587] add Alternate Mod --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 45 +++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 1 + osu.Game/Rulesets/Mods/ModAlternate.cs | 77 +++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs create mode 100644 osu.Game/Rulesets/Mods/ModAlternate.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs new file mode 100644 index 0000000000..9a9074ee52 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModAlternate : ModAlternate + { + private const double flash_duration = 1000; + private OsuAction? lastActionPressed; + private DrawableRuleset ruleset; + + public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + ruleset = drawableRuleset; + base.ApplyToDrawableRuleset(drawableRuleset); + } + + protected override void Reset() + { + lastActionPressed = null; + } + + protected override bool OnPressed(OsuAction key) + { + if (lastActionPressed == key) + { + ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint); + return true; + } + + lastActionPressed = key; + + return false; + } + + protected override void OnReleased(OsuAction key) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 18e4bb259c..7e8974b5ed 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -159,6 +159,7 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()), new OsuModHidden(), new MultiMod(new OsuModFlashlight(), new OsuModBlinds()), + new OsuModAlternate(), }; case ModType.Conversion: diff --git a/osu.Game/Rulesets/Mods/ModAlternate.cs b/osu.Game/Rulesets/Mods/ModAlternate.cs new file mode 100644 index 0000000000..683654f605 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModAlternate.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 System; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModAlternate : Mod + { + public override string Name => @"Alternate"; + public override string Acronym => @"AL"; + public override string Description => @"Never hit the same key twice!"; + public override double ScoreMultiplier => 1.0; + public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) }; + public override ModType Type => ModType.DifficultyIncrease; + public override IconUsage? Icon => FontAwesome.Solid.Keyboard; + } + + public abstract class ModAlternate : ModAlternate, IApplicableToDrawableRuleset, IApplicableToPlayer + where THitObject : HitObject + where TAction : struct + { + public bool CanIntercept => !isBreakTime.Value; + + private IBindable isBreakTime; + + public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this)); + } + + public void ApplyToPlayer(Player player) + { + isBreakTime = player.IsBreakTime.GetBoundCopy(); + isBreakTime.ValueChanged += e => + { + if (e.NewValue) + Reset(); + }; + } + + protected abstract void Reset(); + + protected abstract bool OnPressed(TAction key); + + protected abstract void OnReleased(TAction key); + + private class InputInterceptor : Component, IKeyBindingHandler + { + private readonly ModAlternate mod; + + public InputInterceptor(ModAlternate mod) + { + this.mod = mod; + } + + public bool OnPressed(KeyBindingPressEvent e) + { + return mod.CanIntercept && mod.OnPressed(e.Action); + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + if (mod.CanIntercept) + mod.OnReleased(e.Action); + } + } + } +} From 2326c36836aa317635c869610d7daff18ecbbee3 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Sat, 29 Jan 2022 21:09:36 +0800 Subject: [PATCH 252/587] remove unused method and fix description --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 4 ---- osu.Game/Rulesets/Mods/ModAlternate.cs | 9 ++++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index 9a9074ee52..34802bab43 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -37,9 +37,5 @@ namespace osu.Game.Rulesets.Osu.Mods return false; } - - protected override void OnReleased(OsuAction key) - { - } } } diff --git a/osu.Game/Rulesets/Mods/ModAlternate.cs b/osu.Game/Rulesets/Mods/ModAlternate.cs index 683654f605..17d8b92469 100644 --- a/osu.Game/Rulesets/Mods/ModAlternate.cs +++ b/osu.Game/Rulesets/Mods/ModAlternate.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => @"Alternate"; public override string Acronym => @"AL"; - public override string Description => @"Never hit the same key twice!"; + public override string Description => @"Don't use the same key twice in a row!"; public override double ScoreMultiplier => 1.0; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) }; public override ModType Type => ModType.DifficultyIncrease; @@ -28,6 +28,9 @@ namespace osu.Game.Rulesets.Mods where THitObject : HitObject where TAction : struct { + /// + /// Whether incoming input must be checked by . + /// public bool CanIntercept => !isBreakTime.Value; private IBindable isBreakTime; @@ -51,8 +54,6 @@ namespace osu.Game.Rulesets.Mods protected abstract bool OnPressed(TAction key); - protected abstract void OnReleased(TAction key); - private class InputInterceptor : Component, IKeyBindingHandler { private readonly ModAlternate mod; @@ -69,8 +70,6 @@ namespace osu.Game.Rulesets.Mods public void OnReleased(KeyBindingReleaseEvent e) { - if (mod.CanIntercept) - mod.OnReleased(e.Action); } } } From 98d8b26a9c24dee403ba9d7199d0652910c72c0b Mon Sep 17 00:00:00 2001 From: Nitrous Date: Sat, 29 Jan 2022 21:49:40 +0800 Subject: [PATCH 253/587] move `ModAlternate` to `OsuModAlternate` and check if intro has ended --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 67 ++++++++++++++-- osu.Game/Rulesets/Mods/ModAlternate.cs | 76 ------------------- 2 files changed, 61 insertions(+), 82 deletions(-) delete mode 100644 osu.Game/Rulesets/Mods/ModAlternate.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index 34802bab43..f366481c7d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -1,31 +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.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModAlternate : ModAlternate + public class OsuModAlternate : Mod, IApplicableToDrawableRuleset, IApplicableToPlayer, IUpdatableByPlayfield { + public override string Name => @"Alternate"; + public override string Acronym => @"AL"; + public override string Description => @"Don't use the same key twice in a row!"; + public override double ScoreMultiplier => 1.0; + public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) }; + public override ModType Type => ModType.DifficultyIncrease; + public override IconUsage? Icon => FontAwesome.Solid.Keyboard; + + /// + /// Whether incoming input must be checked by . + /// + public bool CanIntercept => !isBreakTime.Value && introEnded; + + private bool introEnded; + private double earliestStartTime; + private IBindable isBreakTime; private const double flash_duration = 1000; private OsuAction? lastActionPressed; private DrawableRuleset ruleset; - public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { ruleset = drawableRuleset; - base.ApplyToDrawableRuleset(drawableRuleset); + drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this)); + + var firstHitObject = ruleset.Objects.FirstOrDefault(); + earliestStartTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0); } - protected override void Reset() + public void ApplyToPlayer(Player player) { - lastActionPressed = null; + isBreakTime = player.IsBreakTime.GetBoundCopy(); + isBreakTime.ValueChanged += e => + { + if (e.NewValue) + lastActionPressed = null; + }; } - protected override bool OnPressed(OsuAction key) + private bool onPressed(OsuAction key) { if (lastActionPressed == key) { @@ -37,5 +69,28 @@ namespace osu.Game.Rulesets.Osu.Mods return false; } + + public void Update(Playfield playfield) + { + if (!introEnded) + introEnded = playfield.Clock.CurrentTime > earliestStartTime; + } + + private class InputInterceptor : Component, IKeyBindingHandler + { + private readonly OsuModAlternate mod; + + public InputInterceptor(OsuModAlternate mod) + { + this.mod = mod; + } + + public bool OnPressed(KeyBindingPressEvent e) + => mod.CanIntercept && mod.onPressed(e.Action); + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } } } diff --git a/osu.Game/Rulesets/Mods/ModAlternate.cs b/osu.Game/Rulesets/Mods/ModAlternate.cs deleted file mode 100644 index 17d8b92469..0000000000 --- a/osu.Game/Rulesets/Mods/ModAlternate.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; - -namespace osu.Game.Rulesets.Mods -{ - public abstract class ModAlternate : Mod - { - public override string Name => @"Alternate"; - public override string Acronym => @"AL"; - public override string Description => @"Don't use the same key twice in a row!"; - public override double ScoreMultiplier => 1.0; - public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) }; - public override ModType Type => ModType.DifficultyIncrease; - public override IconUsage? Icon => FontAwesome.Solid.Keyboard; - } - - public abstract class ModAlternate : ModAlternate, IApplicableToDrawableRuleset, IApplicableToPlayer - where THitObject : HitObject - where TAction : struct - { - /// - /// Whether incoming input must be checked by . - /// - public bool CanIntercept => !isBreakTime.Value; - - private IBindable isBreakTime; - - public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) - { - drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this)); - } - - public void ApplyToPlayer(Player player) - { - isBreakTime = player.IsBreakTime.GetBoundCopy(); - isBreakTime.ValueChanged += e => - { - if (e.NewValue) - Reset(); - }; - } - - protected abstract void Reset(); - - protected abstract bool OnPressed(TAction key); - - private class InputInterceptor : Component, IKeyBindingHandler - { - private readonly ModAlternate mod; - - public InputInterceptor(ModAlternate mod) - { - this.mod = mod; - } - - public bool OnPressed(KeyBindingPressEvent e) - { - return mod.CanIntercept && mod.OnPressed(e.Action); - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } - } - } -} From d48fae11004b7142a58b8d701c0cbfd073e43992 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Jan 2022 23:13:19 +0900 Subject: [PATCH 254/587] Revert "Remove all EF migrations" This reverts commit bb5b9458e8130a2a5f213e968f5590d00ede3d87. --- .../20171019041408_InitialCreate.Designer.cs | 293 ++++++++++ .../20171019041408_InitialCreate.cs | 311 +++++++++++ ...025071459_AddMissingIndexRules.Designer.cs | 299 ++++++++++ .../20171025071459_AddMissingIndexRules.cs | 80 +++ ...eatmapOnlineIDUniqueConstraint.Designer.cs | 302 ++++++++++ ...5731_AddBeatmapOnlineIDUniqueConstraint.cs | 23 + ...034410_AddRulesetInfoShortName.Designer.cs | 307 +++++++++++ .../20171209034410_AddRulesetInfoShortName.cs | 33 ++ .../20180125143340_Settings.Designer.cs | 329 +++++++++++ .../Migrations/20180125143340_Settings.cs | 55 ++ .../20180131154205_AddMuteBinding.cs | 23 + .../20180219060912_AddSkins.Designer.cs | 379 +++++++++++++ .../Migrations/20180219060912_AddSkins.cs | 71 +++ ...54_RemoveUniqueHashConstraints.Designer.cs | 377 +++++++++++++ ...80529055154_RemoveUniqueHashConstraints.cs | 51 ++ ...111_UpdateTaikoDefaultBindings.Designer.cs | 376 +++++++++++++ ...180621044111_UpdateTaikoDefaultBindings.cs | 17 + ...628011956_RemoveNegativeSetIDs.Designer.cs | 376 +++++++++++++ .../20180628011956_RemoveNegativeSetIDs.cs | 18 + .../20180913080842_AddRankStatus.Designer.cs | 380 +++++++++++++ .../20180913080842_AddRankStatus.cs | 33 ++ ...0181007180454_StandardizePaths.Designer.cs | 380 +++++++++++++ .../20181007180454_StandardizePaths.cs | 26 + ...20181128100659_AddSkinInfoHash.Designer.cs | 387 +++++++++++++ .../20181128100659_AddSkinInfoHash.cs | 41 ++ ...81130113755_AddScoreInfoTables.Designer.cs | 484 ++++++++++++++++ .../20181130113755_AddScoreInfoTables.cs | 112 ++++ ...20190225062029_AddUserIDColumn.Designer.cs | 487 +++++++++++++++++ .../20190225062029_AddUserIDColumn.cs | 22 + .../20190525060824_SkinSettings.Designer.cs | 498 +++++++++++++++++ .../Migrations/20190525060824_SkinSettings.cs | 54 ++ ...AddDateAddedColumnToBeatmapSet.Designer.cs | 489 +++++++++++++++++ ...05091246_AddDateAddedColumnToBeatmapSet.cs | 24 + ...8070844_AddBPMAndLengthColumns.Designer.cs | 504 +++++++++++++++++ .../20190708070844_AddBPMAndLengthColumns.cs | 33 ++ ...20190913104727_AddBeatmapVideo.Designer.cs | 506 +++++++++++++++++ .../20190913104727_AddBeatmapVideo.cs | 22 + ...02094919_RefreshVolumeBindings.Designer.cs | 506 +++++++++++++++++ .../20200302094919_RefreshVolumeBindings.cs | 16 + ...01019224408_AddEpilepsyWarning.Designer.cs | 508 +++++++++++++++++ .../20201019224408_AddEpilepsyWarning.cs | 23 + ...700_RefreshVolumeBindingsAgain.Designer.cs | 506 +++++++++++++++++ ...210412045700_RefreshVolumeBindingsAgain.cs | 16 + ...60743_AddSkinInstantiationInfo.Designer.cs | 508 +++++++++++++++++ ...20210511060743_AddSkinInstantiationInfo.cs | 22 + ...9_AddAuthorIdToBeatmapMetadata.Designer.cs | 511 +++++++++++++++++ ...0514062639_AddAuthorIdToBeatmapMetadata.cs | 23 + ...824185035_AddCountdownSettings.Designer.cs | 513 +++++++++++++++++ .../20210824185035_AddCountdownSettings.cs | 23 + ...11_AddSamplesMatchPlaybackRate.Designer.cs | 515 ++++++++++++++++++ ...10912144011_AddSamplesMatchPlaybackRate.cs | 23 + .../20211020081609_ResetSkinHashes.cs | 23 + .../Migrations/OsuDbContextModelSnapshot.cs | 513 +++++++++++++++++ 53 files changed, 12451 insertions(+) create mode 100644 osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs create mode 100644 osu.Game/Migrations/20171019041408_InitialCreate.cs create mode 100644 osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs create mode 100644 osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs create mode 100644 osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs create mode 100644 osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs create mode 100644 osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs create mode 100644 osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs create mode 100644 osu.Game/Migrations/20180125143340_Settings.Designer.cs create mode 100644 osu.Game/Migrations/20180125143340_Settings.cs create mode 100644 osu.Game/Migrations/20180131154205_AddMuteBinding.cs create mode 100644 osu.Game/Migrations/20180219060912_AddSkins.Designer.cs create mode 100644 osu.Game/Migrations/20180219060912_AddSkins.cs create mode 100644 osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs create mode 100644 osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs create mode 100644 osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs create mode 100644 osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs create mode 100644 osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs create mode 100644 osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs create mode 100644 osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs create mode 100644 osu.Game/Migrations/20180913080842_AddRankStatus.cs create mode 100644 osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs create mode 100644 osu.Game/Migrations/20181007180454_StandardizePaths.cs create mode 100644 osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs create mode 100644 osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs create mode 100644 osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs create mode 100644 osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs create mode 100644 osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs create mode 100644 osu.Game/Migrations/20190225062029_AddUserIDColumn.cs create mode 100644 osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs create mode 100644 osu.Game/Migrations/20190525060824_SkinSettings.cs create mode 100644 osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs create mode 100644 osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs create mode 100644 osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs create mode 100644 osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs create mode 100644 osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs create mode 100644 osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs create mode 100644 osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs create mode 100644 osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs create mode 100644 osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs create mode 100644 osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs create mode 100644 osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs create mode 100644 osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs create mode 100644 osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs create mode 100644 osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs create mode 100644 osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs create mode 100644 osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs create mode 100644 osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs create mode 100644 osu.Game/Migrations/20210824185035_AddCountdownSettings.cs create mode 100644 osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs create mode 100644 osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs create mode 100644 osu.Game/Migrations/20211020081609_ResetSkinHashes.cs create mode 100644 osu.Game/Migrations/OsuDbContextModelSnapshot.cs diff --git a/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs b/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs new file mode 100644 index 0000000000..c751530bf4 --- /dev/null +++ b/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs @@ -0,0 +1,293 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20171019041408_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash"); + + b.HasIndex("MetadataID"); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20171019041408_InitialCreate.cs b/osu.Game/Migrations/20171019041408_InitialCreate.cs new file mode 100644 index 0000000000..9b6881f98c --- /dev/null +++ b/osu.Game/Migrations/20171019041408_InitialCreate.cs @@ -0,0 +1,311 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "BeatmapDifficulty", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ApproachRate = table.Column(type: "REAL", nullable: false), + CircleSize = table.Column(type: "REAL", nullable: false), + DrainRate = table.Column(type: "REAL", nullable: false), + OverallDifficulty = table.Column(type: "REAL", nullable: false), + SliderMultiplier = table.Column(type: "REAL", nullable: false), + SliderTickRate = table.Column(type: "REAL", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeatmapDifficulty", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "BeatmapMetadata", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Artist = table.Column(type: "TEXT", nullable: true), + ArtistUnicode = table.Column(type: "TEXT", nullable: true), + AudioFile = table.Column(type: "TEXT", nullable: true), + Author = table.Column(type: "TEXT", nullable: true), + BackgroundFile = table.Column(type: "TEXT", nullable: true), + PreviewTime = table.Column(type: "INTEGER", nullable: false), + Source = table.Column(type: "TEXT", nullable: true), + Tags = table.Column(type: "TEXT", nullable: true), + Title = table.Column(type: "TEXT", nullable: true), + TitleUnicode = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BeatmapMetadata", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "FileInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Hash = table.Column(type: "TEXT", nullable: true), + ReferenceCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FileInfo", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "KeyBinding", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Action = table.Column(type: "INTEGER", nullable: false), + Keys = table.Column(type: "TEXT", nullable: true), + RulesetID = table.Column(type: "INTEGER", nullable: true), + Variant = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_KeyBinding", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "RulesetInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Available = table.Column(type: "INTEGER", nullable: false), + InstantiationInfo = table.Column(type: "TEXT", nullable: true), + Name = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RulesetInfo", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "BeatmapSetInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DeletePending = table.Column(type: "INTEGER", nullable: false), + Hash = table.Column(type: "TEXT", nullable: true), + MetadataID = table.Column(type: "INTEGER", nullable: true), + OnlineBeatmapSetID = table.Column(type: "INTEGER", nullable: true), + Protected = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeatmapSetInfo", x => x.ID); + table.ForeignKey( + name: "FK_BeatmapSetInfo_BeatmapMetadata_MetadataID", + column: x => x.MetadataID, + principalTable: "BeatmapMetadata", + principalColumn: "ID", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "BeatmapInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AudioLeadIn = table.Column(type: "INTEGER", nullable: false), + BaseDifficultyID = table.Column(type: "INTEGER", nullable: false), + BeatDivisor = table.Column(type: "INTEGER", nullable: false), + BeatmapSetInfoID = table.Column(type: "INTEGER", nullable: false), + Countdown = table.Column(type: "INTEGER", nullable: false), + DistanceSpacing = table.Column(type: "REAL", nullable: false), + GridSize = table.Column(type: "INTEGER", nullable: false), + Hash = table.Column(type: "TEXT", nullable: true), + Hidden = table.Column(type: "INTEGER", nullable: false), + LetterboxInBreaks = table.Column(type: "INTEGER", nullable: false), + MD5Hash = table.Column(type: "TEXT", nullable: true), + MetadataID = table.Column(type: "INTEGER", nullable: true), + OnlineBeatmapID = table.Column(type: "INTEGER", nullable: true), + Path = table.Column(type: "TEXT", nullable: true), + RulesetID = table.Column(type: "INTEGER", nullable: false), + SpecialStyle = table.Column(type: "INTEGER", nullable: false), + StackLeniency = table.Column(type: "REAL", nullable: false), + StarDifficulty = table.Column(type: "REAL", nullable: false), + StoredBookmarks = table.Column(type: "TEXT", nullable: true), + TimelineZoom = table.Column(type: "REAL", nullable: false), + Version = table.Column(type: "TEXT", nullable: true), + WidescreenStoryboard = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeatmapInfo", x => x.ID); + table.ForeignKey( + name: "FK_BeatmapInfo_BeatmapDifficulty_BaseDifficultyID", + column: x => x.BaseDifficultyID, + principalTable: "BeatmapDifficulty", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_BeatmapInfo_BeatmapSetInfo_BeatmapSetInfoID", + column: x => x.BeatmapSetInfoID, + principalTable: "BeatmapSetInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_BeatmapInfo_BeatmapMetadata_MetadataID", + column: x => x.MetadataID, + principalTable: "BeatmapMetadata", + principalColumn: "ID", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_BeatmapInfo_RulesetInfo_RulesetID", + column: x => x.RulesetID, + principalTable: "RulesetInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BeatmapSetFileInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + BeatmapSetInfoID = table.Column(type: "INTEGER", nullable: false), + FileInfoID = table.Column(type: "INTEGER", nullable: false), + Filename = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeatmapSetFileInfo", x => x.ID); + table.ForeignKey( + name: "FK_BeatmapSetFileInfo_BeatmapSetInfo_BeatmapSetInfoID", + column: x => x.BeatmapSetInfoID, + principalTable: "BeatmapSetInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_BeatmapSetFileInfo_FileInfo_FileInfoID", + column: x => x.FileInfoID, + principalTable: "FileInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_BaseDifficultyID", + table: "BeatmapInfo", + column: "BaseDifficultyID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_BeatmapSetInfoID", + table: "BeatmapInfo", + column: "BeatmapSetInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo", + column: "Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo", + column: "MD5Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MetadataID", + table: "BeatmapInfo", + column: "MetadataID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_RulesetID", + table: "BeatmapInfo", + column: "RulesetID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetFileInfo_BeatmapSetInfoID", + table: "BeatmapSetFileInfo", + column: "BeatmapSetInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetFileInfo_FileInfoID", + table: "BeatmapSetFileInfo", + column: "FileInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_DeletePending", + table: "BeatmapSetInfo", + column: "DeletePending"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_Hash", + table: "BeatmapSetInfo", + column: "Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_MetadataID", + table: "BeatmapSetInfo", + column: "MetadataID"); + + migrationBuilder.CreateIndex( + name: "IX_FileInfo_Hash", + table: "FileInfo", + column: "Hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_FileInfo_ReferenceCount", + table: "FileInfo", + column: "ReferenceCount"); + + migrationBuilder.CreateIndex( + name: "IX_KeyBinding_Action", + table: "KeyBinding", + column: "Action"); + + migrationBuilder.CreateIndex( + name: "IX_KeyBinding_Variant", + table: "KeyBinding", + column: "Variant"); + + migrationBuilder.CreateIndex( + name: "IX_RulesetInfo_Available", + table: "RulesetInfo", + column: "Available"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BeatmapInfo"); + + migrationBuilder.DropTable( + name: "BeatmapSetFileInfo"); + + migrationBuilder.DropTable( + name: "KeyBinding"); + + migrationBuilder.DropTable( + name: "BeatmapDifficulty"); + + migrationBuilder.DropTable( + name: "RulesetInfo"); + + migrationBuilder.DropTable( + name: "BeatmapSetInfo"); + + migrationBuilder.DropTable( + name: "FileInfo"); + + migrationBuilder.DropTable( + name: "BeatmapMetadata"); + } + } +} diff --git a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs new file mode 100644 index 0000000000..4cd234f2ef --- /dev/null +++ b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs @@ -0,0 +1,299 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20171025071459_AddMissingIndexRules")] + partial class AddMissingIndexRules + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs new file mode 100644 index 0000000000..c9fc59c5a2 --- /dev/null +++ b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddMissingIndexRules : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_BeatmapSetInfo_Hash", + table: "BeatmapSetInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_Hash", + table: "BeatmapSetInfo", + column: "Hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_OnlineBeatmapSetID", + table: "BeatmapSetInfo", + column: "OnlineBeatmapSetID", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo", + column: "Hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo", + column: "MD5Hash", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_BeatmapSetInfo_Hash", + table: "BeatmapSetInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapSetInfo_OnlineBeatmapSetID", + table: "BeatmapSetInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_Hash", + table: "BeatmapSetInfo", + column: "Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo", + column: "Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo", + column: "MD5Hash"); + } + } +} diff --git a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs new file mode 100644 index 0000000000..006acf12cd --- /dev/null +++ b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs @@ -0,0 +1,302 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20171119065731_AddBeatmapOnlineIDUniqueConstraint")] + partial class AddBeatmapOnlineIDUniqueConstraint + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs new file mode 100644 index 0000000000..084ae67940 --- /dev/null +++ b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddBeatmapOnlineIDUniqueConstraint : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_OnlineBeatmapID", + table: "BeatmapInfo", + column: "OnlineBeatmapID", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_OnlineBeatmapID", + table: "BeatmapInfo"); + } + } +} diff --git a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs new file mode 100644 index 0000000000..fc2496bc24 --- /dev/null +++ b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs @@ -0,0 +1,307 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20171209034410_AddRulesetInfoShortName")] + partial class AddRulesetInfoShortName + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs new file mode 100644 index 0000000000..09cf0af89c --- /dev/null +++ b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddRulesetInfoShortName : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ShortName", + table: "RulesetInfo", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_RulesetInfo_ShortName", + table: "RulesetInfo", + column: "ShortName", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_RulesetInfo_ShortName", + table: "RulesetInfo"); + + migrationBuilder.DropColumn( + name: "ShortName", + table: "RulesetInfo"); + } + } +} diff --git a/osu.Game/Migrations/20180125143340_Settings.Designer.cs b/osu.Game/Migrations/20180125143340_Settings.Designer.cs new file mode 100644 index 0000000000..4bb599eec1 --- /dev/null +++ b/osu.Game/Migrations/20180125143340_Settings.Designer.cs @@ -0,0 +1,329 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180125143340_Settings")] + partial class Settings + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180125143340_Settings.cs b/osu.Game/Migrations/20180125143340_Settings.cs new file mode 100644 index 0000000000..166d3c086d --- /dev/null +++ b/osu.Game/Migrations/20180125143340_Settings.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class Settings : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_KeyBinding_Variant", + table: "KeyBinding"); + + migrationBuilder.CreateTable( + name: "Settings", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Key = table.Column(type: "TEXT", nullable: false), + RulesetID = table.Column(type: "INTEGER", nullable: true), + Value = table.Column(type: "TEXT", nullable: true), + Variant = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Settings", x => x.ID); + }); + + migrationBuilder.CreateIndex( + name: "IX_KeyBinding_RulesetID_Variant", + table: "KeyBinding", + columns: new[] { "RulesetID", "Variant" }); + + migrationBuilder.CreateIndex( + name: "IX_Settings_RulesetID_Variant", + table: "Settings", + columns: new[] { "RulesetID", "Variant" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Settings"); + + migrationBuilder.DropIndex( + name: "IX_KeyBinding_RulesetID_Variant", + table: "KeyBinding"); + + migrationBuilder.CreateIndex( + name: "IX_KeyBinding_Variant", + table: "KeyBinding", + column: "Variant"); + } + } +} diff --git a/osu.Game/Migrations/20180131154205_AddMuteBinding.cs b/osu.Game/Migrations/20180131154205_AddMuteBinding.cs new file mode 100644 index 0000000000..5564a30bbf --- /dev/null +++ b/osu.Game/Migrations/20180131154205_AddMuteBinding.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using osu.Game.Database; +using osu.Game.Input.Bindings; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180131154205_AddMuteBinding")] + public partial class AddMuteBinding : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql($"UPDATE KeyBinding SET Action = Action + 1 WHERE RulesetID IS NULL AND Variant IS NULL AND Action >= {(int)GlobalAction.ToggleMute}"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql($"DELETE FROM KeyBinding WHERE RulesetID IS NULL AND Variant IS NULL AND Action = {(int)GlobalAction.ToggleMute}"); + migrationBuilder.Sql($"UPDATE KeyBinding SET Action = Action - 1 WHERE RulesetID IS NULL AND Variant IS NULL AND Action > {(int)GlobalAction.ToggleMute}"); + } + } +} diff --git a/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs new file mode 100644 index 0000000000..cdc4ef2e66 --- /dev/null +++ b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs @@ -0,0 +1,379 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180219060912_AddSkins")] + partial class AddSkins + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180219060912_AddSkins.cs b/osu.Game/Migrations/20180219060912_AddSkins.cs new file mode 100644 index 0000000000..a0270ab0fd --- /dev/null +++ b/osu.Game/Migrations/20180219060912_AddSkins.cs @@ -0,0 +1,71 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddSkins : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SkinInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Creator = table.Column(type: "TEXT", nullable: true), + DeletePending = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SkinInfo", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "SkinFileInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + FileInfoID = table.Column(type: "INTEGER", nullable: false), + Filename = table.Column(type: "TEXT", nullable: false), + SkinInfoID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SkinFileInfo", x => x.ID); + table.ForeignKey( + name: "FK_SkinFileInfo_FileInfo_FileInfoID", + column: x => x.FileInfoID, + principalTable: "FileInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SkinFileInfo_SkinInfo_SkinInfoID", + column: x => x.SkinInfoID, + principalTable: "SkinInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_SkinFileInfo_FileInfoID", + table: "SkinFileInfo", + column: "FileInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_SkinFileInfo_SkinInfoID", + table: "SkinFileInfo", + column: "SkinInfoID"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SkinFileInfo"); + + migrationBuilder.DropTable( + name: "SkinInfo"); + } + } +} diff --git a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs new file mode 100644 index 0000000000..f28408bfb3 --- /dev/null +++ b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs @@ -0,0 +1,377 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180529055154_RemoveUniqueHashConstraints")] + partial class RemoveUniqueHashConstraints + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.3-rtm-10026"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs new file mode 100644 index 0000000000..27269cc5fc --- /dev/null +++ b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class RemoveUniqueHashConstraints : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo", + column: "Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo", + column: "MD5Hash"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo", + column: "Hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo", + column: "MD5Hash", + unique: true); + } + } +} diff --git a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs new file mode 100644 index 0000000000..aaa11e88b6 --- /dev/null +++ b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs @@ -0,0 +1,376 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180621044111_UpdateTaikoDefaultBindings")] + partial class UpdateTaikoDefaultBindings + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.1-rtm-30846"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs new file mode 100644 index 0000000000..71304ea979 --- /dev/null +++ b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class UpdateTaikoDefaultBindings : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DELETE FROM KeyBinding WHERE RulesetID = 1"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // we can't really tell if these should be restored or not, so let's just not do so. + } + } +} diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs new file mode 100644 index 0000000000..7eeacd56d7 --- /dev/null +++ b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs @@ -0,0 +1,376 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180628011956_RemoveNegativeSetIDs")] + partial class RemoveNegativeSetIDs + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.1-rtm-30846"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs new file mode 100644 index 0000000000..506d65f761 --- /dev/null +++ b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class RemoveNegativeSetIDs : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // There was a change that beatmaps were being loaded with "-1" online IDs, which is completely incorrect. + // This ensures there will not be unique key conflicts as a result of these incorrectly imported beatmaps. + migrationBuilder.Sql("UPDATE BeatmapSetInfo SET OnlineBeatmapSetID = null WHERE OnlineBeatmapSetID <= 0"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs b/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs new file mode 100644 index 0000000000..5ab43da046 --- /dev/null +++ b/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs @@ -0,0 +1,380 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180913080842_AddRankStatus")] + partial class AddRankStatus + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.2-rtm-30932"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180913080842_AddRankStatus.cs b/osu.Game/Migrations/20180913080842_AddRankStatus.cs new file mode 100644 index 0000000000..bba4944bb7 --- /dev/null +++ b/osu.Game/Migrations/20180913080842_AddRankStatus.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddRankStatus : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Status", + table: "BeatmapSetInfo", + nullable: false, + defaultValue: -3); // NONE + + migrationBuilder.AddColumn( + name: "Status", + table: "BeatmapInfo", + nullable: false, + defaultValue: -3); // NONE + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Status", + table: "BeatmapSetInfo"); + + migrationBuilder.DropColumn( + name: "Status", + table: "BeatmapInfo"); + } + } +} diff --git a/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs b/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs new file mode 100644 index 0000000000..b387a45ecf --- /dev/null +++ b/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs @@ -0,0 +1,380 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20181007180454_StandardizePaths")] + partial class StandardizePaths + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.3-rtm-32065"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20181007180454_StandardizePaths.cs b/osu.Game/Migrations/20181007180454_StandardizePaths.cs new file mode 100644 index 0000000000..274b8030a9 --- /dev/null +++ b/osu.Game/Migrations/20181007180454_StandardizePaths.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using System.IO; + +namespace osu.Game.Migrations +{ + public partial class StandardizePaths : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + string windowsStyle = @"\"; + string standardized = "/"; + + // Escaping \ does not seem to be needed. + migrationBuilder.Sql($"UPDATE `BeatmapInfo` SET `Path` = REPLACE(`Path`, '{windowsStyle}', '{standardized}')"); + migrationBuilder.Sql($"UPDATE `BeatmapMetadata` SET `AudioFile` = REPLACE(`AudioFile`, '{windowsStyle}', '{standardized}')"); + migrationBuilder.Sql($"UPDATE `BeatmapMetadata` SET `BackgroundFile` = REPLACE(`BackgroundFile`, '{windowsStyle}', '{standardized}')"); + migrationBuilder.Sql($"UPDATE `BeatmapSetFileInfo` SET `Filename` = REPLACE(`Filename`, '{windowsStyle}', '{standardized}')"); + migrationBuilder.Sql($"UPDATE `SkinFileInfo` SET `Filename` = REPLACE(`Filename`, '{windowsStyle}', '{standardized}')"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs new file mode 100644 index 0000000000..120674671a --- /dev/null +++ b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs @@ -0,0 +1,387 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20181128100659_AddSkinInfoHash")] + partial class AddSkinInfoHash + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.4-rtm-31024"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs new file mode 100644 index 0000000000..860264a7dd --- /dev/null +++ b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddSkinInfoHash : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Hash", + table: "SkinInfo", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_SkinInfo_DeletePending", + table: "SkinInfo", + column: "DeletePending"); + + migrationBuilder.CreateIndex( + name: "IX_SkinInfo_Hash", + table: "SkinInfo", + column: "Hash", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_SkinInfo_DeletePending", + table: "SkinInfo"); + + migrationBuilder.DropIndex( + name: "IX_SkinInfo_Hash", + table: "SkinInfo"); + + migrationBuilder.DropColumn( + name: "Hash", + table: "SkinInfo"); + } + } +} diff --git a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs new file mode 100644 index 0000000000..eee53182ce --- /dev/null +++ b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs @@ -0,0 +1,484 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20181130113755_AddScoreInfoTables")] + partial class AddScoreInfoTables + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.4-rtm-31024"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany() + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs new file mode 100644 index 0000000000..2b6f94c5a4 --- /dev/null +++ b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs @@ -0,0 +1,112 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddScoreInfoTables : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ScoreInfo", + columns: table => new + { + ID = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Rank = table.Column(nullable: false), + TotalScore = table.Column(nullable: false), + Accuracy = table.Column(type: "DECIMAL(1,4)", nullable: false), + PP = table.Column(nullable: true), + MaxCombo = table.Column(nullable: false), + Combo = table.Column(nullable: false), + RulesetID = table.Column(nullable: false), + Mods = table.Column(nullable: true), + User = table.Column(nullable: true), + BeatmapInfoID = table.Column(nullable: false), + OnlineScoreID = table.Column(nullable: true), + Date = table.Column(nullable: false), + Statistics = table.Column(nullable: true), + Hash = table.Column(nullable: true), + DeletePending = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ScoreInfo", x => x.ID); + table.ForeignKey( + name: "FK_ScoreInfo_BeatmapInfo_BeatmapInfoID", + column: x => x.BeatmapInfoID, + principalTable: "BeatmapInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ScoreInfo_RulesetInfo_RulesetID", + column: x => x.RulesetID, + principalTable: "RulesetInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ScoreFileInfo", + columns: table => new + { + ID = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + FileInfoID = table.Column(nullable: false), + Filename = table.Column(nullable: false), + ScoreInfoID = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ScoreFileInfo", x => x.ID); + table.ForeignKey( + name: "FK_ScoreFileInfo_FileInfo_FileInfoID", + column: x => x.FileInfoID, + principalTable: "FileInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ScoreFileInfo_ScoreInfo_ScoreInfoID", + column: x => x.ScoreInfoID, + principalTable: "ScoreInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_ScoreFileInfo_FileInfoID", + table: "ScoreFileInfo", + column: "FileInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_ScoreFileInfo_ScoreInfoID", + table: "ScoreFileInfo", + column: "ScoreInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_ScoreInfo_BeatmapInfoID", + table: "ScoreInfo", + column: "BeatmapInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_ScoreInfo_OnlineScoreID", + table: "ScoreInfo", + column: "OnlineScoreID", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ScoreInfo_RulesetID", + table: "ScoreInfo", + column: "RulesetID"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ScoreFileInfo"); + + migrationBuilder.DropTable( + name: "ScoreInfo"); + } + } +} diff --git a/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs b/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs new file mode 100644 index 0000000000..8e1e3a59f3 --- /dev/null +++ b/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs @@ -0,0 +1,487 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20190225062029_AddUserIDColumn")] + partial class AddUserIDColumn + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.1-servicing-10028"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs b/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs new file mode 100644 index 0000000000..0720e0eac7 --- /dev/null +++ b/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddUserIDColumn : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UserID", + table: "ScoreInfo", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UserID", + table: "ScoreInfo"); + } + } +} diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs b/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs new file mode 100644 index 0000000000..348c42adb9 --- /dev/null +++ b/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs @@ -0,0 +1,498 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20190525060824_SkinSettings")] + partial class SkinSettings + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.4-servicing-10062"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.cs b/osu.Game/Migrations/20190525060824_SkinSettings.cs new file mode 100644 index 0000000000..99237419b7 --- /dev/null +++ b/osu.Game/Migrations/20190525060824_SkinSettings.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class SkinSettings : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@"create table Settings_dg_tmp + ( + ID INTEGER not null + constraint PK_Settings + primary key autoincrement, + Key TEXT not null, + RulesetID INTEGER, + Value TEXT, + Variant INTEGER, + SkinInfoID int + constraint Settings_SkinInfo_ID_fk + references SkinInfo + on delete restrict + ); + + insert into Settings_dg_tmp(ID, Key, RulesetID, Value, Variant) select ID, Key, RulesetID, Value, Variant from Settings; + + drop table Settings; + + alter table Settings_dg_tmp rename to Settings; + + create index IX_Settings_RulesetID_Variant + on Settings (RulesetID, Variant); + + create index Settings_SkinInfoID_index + on Settings (SkinInfoID); + + "); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Settings_SkinInfo_SkinInfoID", + table: "Settings"); + + migrationBuilder.DropIndex( + name: "IX_Settings_SkinInfoID", + table: "Settings"); + + migrationBuilder.DropColumn( + name: "SkinInfoID", + table: "Settings"); + } + } +} diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs new file mode 100644 index 0000000000..9477369aa0 --- /dev/null +++ b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs @@ -0,0 +1,489 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20190605091246_AddDateAddedColumnToBeatmapSet")] + partial class AddDateAddedColumnToBeatmapSet + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.4-servicing-10062"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs new file mode 100644 index 0000000000..55dc18b6a3 --- /dev/null +++ b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddDateAddedColumnToBeatmapSet : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DateAdded", + table: "BeatmapSetInfo", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DateAdded", + table: "BeatmapSetInfo"); + } + } +} diff --git a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs new file mode 100644 index 0000000000..c5fcc16f84 --- /dev/null +++ b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs @@ -0,0 +1,504 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20190708070844_AddBPMAndLengthColumns")] + partial class AddBPMAndLengthColumns + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.4-servicing-10062"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs new file mode 100644 index 0000000000..f5963ebf5e --- /dev/null +++ b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddBPMAndLengthColumns : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BPM", + table: "BeatmapInfo", + nullable: false, + defaultValue: 0.0); + + migrationBuilder.AddColumn( + name: "Length", + table: "BeatmapInfo", + nullable: false, + defaultValue: 0.0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BPM", + table: "BeatmapInfo"); + + migrationBuilder.DropColumn( + name: "Length", + table: "BeatmapInfo"); + } + } +} diff --git a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs new file mode 100644 index 0000000000..826233a2b0 --- /dev/null +++ b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs @@ -0,0 +1,506 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20190913104727_AddBeatmapVideo")] + partial class AddBeatmapVideo + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.Property("VideoFile"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs new file mode 100644 index 0000000000..9ed0943acd --- /dev/null +++ b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddBeatmapVideo : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "VideoFile", + table: "BeatmapMetadata", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "VideoFile", + table: "BeatmapMetadata"); + } + } +} diff --git a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs new file mode 100644 index 0000000000..22316b0380 --- /dev/null +++ b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs @@ -0,0 +1,506 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20200302094919_RefreshVolumeBindings")] + partial class RefreshVolumeBindings + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.Property("VideoFile"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs new file mode 100644 index 0000000000..ec4475971c --- /dev/null +++ b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class RefreshVolumeBindings : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DELETE FROM KeyBinding WHERE action in (6,7)"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs new file mode 100644 index 0000000000..1c05de832e --- /dev/null +++ b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs @@ -0,0 +1,508 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20201019224408_AddEpilepsyWarning")] + partial class AddEpilepsyWarning + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("EpilepsyWarning"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.Property("VideoFile"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs new file mode 100644 index 0000000000..be6968aa5d --- /dev/null +++ b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddEpilepsyWarning : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EpilepsyWarning", + table: "BeatmapInfo", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EpilepsyWarning", + table: "BeatmapInfo"); + } + } +} diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs new file mode 100644 index 0000000000..2c100d39b9 --- /dev/null +++ b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs @@ -0,0 +1,506 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20210412045700_RefreshVolumeBindingsAgain")] + partial class RefreshVolumeBindingsAgain + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.Property("VideoFile"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs new file mode 100644 index 0000000000..155d6670a8 --- /dev/null +++ b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class RefreshVolumeBindingsAgain : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DELETE FROM KeyBinding WHERE action in (6,7)"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs new file mode 100644 index 0000000000..b808c648da --- /dev/null +++ b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs @@ -0,0 +1,508 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20210511060743_AddSkinInstantiationInfo")] + partial class AddSkinInstantiationInfo + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("EpilepsyWarning"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs new file mode 100644 index 0000000000..1d5b0769a4 --- /dev/null +++ b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddSkinInstantiationInfo : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "InstantiationInfo", + table: "SkinInfo", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "InstantiationInfo", + table: "SkinInfo"); + } + } +} diff --git a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs new file mode 100644 index 0000000000..89bab3a0fa --- /dev/null +++ b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs @@ -0,0 +1,511 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20210514062639_AddAuthorIdToBeatmapMetadata")] + partial class AddAuthorIdToBeatmapMetadata + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("EpilepsyWarning"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorID") + .HasColumnName("AuthorID"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs new file mode 100644 index 0000000000..98fe9b5e13 --- /dev/null +++ b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddAuthorIdToBeatmapMetadata : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AuthorID", + table: "BeatmapMetadata", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AuthorID", + table: "BeatmapMetadata"); + } + } +} diff --git a/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs b/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs new file mode 100644 index 0000000000..afeb42130d --- /dev/null +++ b/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs @@ -0,0 +1,513 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20210824185035_AddCountdownSettings")] + partial class AddCountdownSettings + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("CountdownOffset"); + + b.Property("DistanceSpacing"); + + b.Property("EpilepsyWarning"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorID") + .HasColumnName("AuthorID"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs b/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs new file mode 100644 index 0000000000..564f5f4520 --- /dev/null +++ b/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddCountdownSettings : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CountdownOffset", + table: "BeatmapInfo", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CountdownOffset", + table: "BeatmapInfo"); + } + } +} diff --git a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs new file mode 100644 index 0000000000..6e53d7fae0 --- /dev/null +++ b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs @@ -0,0 +1,515 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20210912144011_AddSamplesMatchPlaybackRate")] + partial class AddSamplesMatchPlaybackRate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("CountdownOffset"); + + b.Property("DistanceSpacing"); + + b.Property("EpilepsyWarning"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SamplesMatchPlaybackRate"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorID") + .HasColumnName("AuthorID"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs new file mode 100644 index 0000000000..bf3f855d5f --- /dev/null +++ b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddSamplesMatchPlaybackRate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SamplesMatchPlaybackRate", + table: "BeatmapInfo", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SamplesMatchPlaybackRate", + table: "BeatmapInfo"); + } + } +} diff --git a/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs b/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs new file mode 100644 index 0000000000..6d53c019ec --- /dev/null +++ b/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20211020081609_ResetSkinHashes")] + public partial class ResetSkinHashes : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql($"UPDATE SkinInfo SET Hash = null"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs new file mode 100644 index 0000000000..036c26cb0a --- /dev/null +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -0,0 +1,513 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + partial class OsuDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("CountdownOffset"); + + b.Property("DistanceSpacing"); + + b.Property("EpilepsyWarning"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SamplesMatchPlaybackRate"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorID") + .HasColumnName("AuthorID"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} From 1a14ccc7ee37f2daa4b3500483668621b7c7abef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Jan 2022 22:42:34 +0900 Subject: [PATCH 255/587] Run EF migrations before migrating to realm Turns out that there are more than zero users that are upgrading from old databases. I think we probably want to support this for now. Tested against database in https://github.com/ppy/osu/discussions/16700 and one other I had locally, both work correctly. --- osu.Game/Database/EFToRealmMigrator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index a53704df9d..0bb5388d55 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -133,6 +133,8 @@ namespace osu.Game.Database r.RemoveAll(); }); + ef.Migrate(); + migrateSettings(ef); migrateSkins(ef); migrateBeatmaps(ef); From 24f9ef4005827338702e3e8766a835b755c7a5d0 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Sat, 29 Jan 2022 22:31:04 +0800 Subject: [PATCH 256/587] make xmldoc more verbose --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index f366481c7d..9eeb060135 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override IconUsage? Icon => FontAwesome.Solid.Keyboard; /// - /// Whether incoming input must be checked by . + /// Whether incoming input must be checked by before it is passed to gameplay. /// public bool CanIntercept => !isBreakTime.Value && introEnded; From b4e516c535032f91c70f67e0ae8aebbe156d8d3e Mon Sep 17 00:00:00 2001 From: Nitrous Date: Sat, 29 Jan 2022 22:31:24 +0800 Subject: [PATCH 257/587] allow test scenes to specify replays manually --- osu.Game/Tests/Visual/ModTestScene.cs | 33 +++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index a71d008eb9..524482237a 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -5,8 +5,12 @@ using System; using System.Collections.Generic; using JetBrains.Annotations; using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays; +using osu.Game.Scoring; namespace osu.Game.Tests.Visual { @@ -50,18 +54,37 @@ namespace osu.Game.Tests.Visual return CreateModPlayer(ruleset); } - protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(AllowFail); + protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(currentTestData, AllowFail); protected class ModTestPlayer : TestPlayer { private readonly bool allowFail; + private ModTestData currentTestData; protected override bool CheckModsAllowFailure() => allowFail; - public ModTestPlayer(bool allowFail) + public ModTestPlayer(ModTestData data, bool allowFail) : base(false, false) { this.allowFail = allowFail; + currentTestData = data; + } + + protected override void PrepareReplay() + { + if (currentTestData.Autoplay && currentTestData.Frames?.Count > 0) + throw new InvalidOperationException(@$"{nameof(ModTestData.Autoplay)} must be false when {nameof(ModTestData.Frames)} is specified."); + + if (currentTestData.Frames != null) + { + DrawableRuleset?.SetReplayScore(new Score + { + Replay = new Replay { Frames = currentTestData.Frames }, + ScoreInfo = new ScoreInfo { User = new APIUser { Username = @"Test" } }, + }); + } + + base.PrepareReplay(); } } @@ -72,6 +95,12 @@ namespace osu.Game.Tests.Visual /// public bool Autoplay = true; + /// + /// The frames to use for replay. must be set to false. + /// + [CanBeNull] + public List Frames; + /// /// The beatmap for this test case. /// From 1087d8b1cedfaad85e048151e20638c6cf18ff18 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Sat, 29 Jan 2022 22:31:31 +0800 Subject: [PATCH 258/587] add tests --- .../Mods/TestSceneOsuModAlternate.cs | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs new file mode 100644 index 0000000000..7b3eaa7ffd --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs @@ -0,0 +1,154 @@ +// 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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModAlternate : OsuModTestScene + { + [Test] + public void TestInputAtIntro() => CreateModTest(new ModTestData + { + Mod = new OsuModAlternate(), + PassCondition = () => Player.ScoreProcessor.Combo.Value == 1, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new HitCircle + { + StartTime = 1000, + Position = new Vector2(100), + }, + }, + }, + Frames = new List + { + new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton), + new OsuReplayFrame(501, new Vector2(200)), + new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton), + } + }); + + [Test] + public void TestInputAlternating() => CreateModTest(new ModTestData + { + Mod = new OsuModAlternate(), + PassCondition = () => Player.ScoreProcessor.Combo.Value == 4, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new HitCircle + { + StartTime = 500, + Position = new Vector2(100), + }, + new HitCircle + { + StartTime = 1000, + Position = new Vector2(200, 100), + }, + new HitCircle + { + StartTime = 1500, + Position = new Vector2(300, 100), + }, + new HitCircle + { + StartTime = 2000, + Position = new Vector2(400, 100), + }, + }, + }, + Frames = new List + { + new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), + new OsuReplayFrame(501, new Vector2(100)), + new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton), + new OsuReplayFrame(1001, new Vector2(200, 100)), + new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton), + new OsuReplayFrame(1501, new Vector2(300, 100)), + new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton), + new OsuReplayFrame(2001, new Vector2(400, 100)), + } + }); + + [Test] + public void TestInputSingular() => CreateModTest(new ModTestData + { + Mod = new OsuModAlternate(), + PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new HitCircle + { + StartTime = 500, + Position = new Vector2(100), + }, + new HitCircle + { + StartTime = 1000, + Position = new Vector2(200, 100), + }, + }, + }, + Frames = new List + { + new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), + new OsuReplayFrame(501, new Vector2(100)), + new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton), + } + }); + + [Test] + public void TestInputSingularWithBreak() => CreateModTest(new ModTestData + { + Mod = new OsuModAlternate(), + PassCondition = () => Player.ScoreProcessor.Combo.Value == 2, + Autoplay = false, + Beatmap = new Beatmap + { + Breaks = new List + { + new BreakPeriod(500, 2250), + }, + HitObjects = new List + { + new HitCircle + { + StartTime = 500, + Position = new Vector2(100), + }, + new HitCircle + { + StartTime = 2500, + Position = new Vector2(100), + } + } + }, + Frames = new List + { + new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), + new OsuReplayFrame(501, new Vector2(100)), + new OsuReplayFrame(2500, new Vector2(100), OsuAction.LeftButton), + new OsuReplayFrame(2501, new Vector2(100)), + } + }); + } +} From a8eb3f95df2501659941c8d4d242fd8d08b897c8 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Sat, 29 Jan 2022 22:54:17 +0800 Subject: [PATCH 259/587] add readonly modifier --- osu.Game/Tests/Visual/ModTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 524482237a..96fafa8727 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual protected class ModTestPlayer : TestPlayer { private readonly bool allowFail; - private ModTestData currentTestData; + private readonly ModTestData currentTestData; protected override bool CheckModsAllowFailure() => allowFail; From c6d303a5b44ba2f0d0c221178e9abab50dc2fbaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 21:16:29 +0900 Subject: [PATCH 260/587] Add xmldoc to `Leaderboard` class --- osu.Game/Online/Leaderboards/Leaderboard.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 515cc6fd73..e2a52b5db9 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -23,6 +23,12 @@ using osuTK.Graphics; namespace osu.Game.Online.Leaderboards { + /// + /// A leaderboard which displays a scrolling list of top scores, along with a single "user best" + /// for the local user. + /// + /// The scope of the leaderboard (ie. global or local). + /// The score model class. public abstract class Leaderboard : Container { private const double fade_duration = 300; From aee93934d5c42792b41ef3b13c7baa748b7c3ee0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 21:22:09 +0900 Subject: [PATCH 261/587] Rename methods to make more sense (and always run through `AddOnce`) --- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 +- osu.Game/Online/Leaderboards/Leaderboard.cs | 10 +++++----- .../OnlinePlay/Match/Components/MatchLeaderboard.cs | 2 +- .../OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- .../Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 10 +++++----- osu.Game/Screens/Select/PlayBeatmapDetailArea.cs | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 4826d2fb33..4938fd1271 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.UserInterface leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables leaderboard.BeatmapInfo = beatmapInfo; - leaderboard.RefreshScores(); // Required in the case that the beatmap hasn't changed + leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed }); [SetUpSteps] diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index e2a52b5db9..cd67fc7301 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -132,7 +132,7 @@ namespace osu.Game.Online.Leaderboards return; scope = value; - RefreshScores(); + RefetchScores(); } } @@ -160,7 +160,7 @@ namespace osu.Game.Online.Leaderboards case PlaceholderState.NetworkFailure: replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) { - Action = RefreshScores + Action = RefetchScores }); break; @@ -272,15 +272,15 @@ namespace osu.Game.Online.Leaderboards case APIState.Online: case APIState.Offline: if (IsOnlineScope) - RefreshScores(); + RefetchScores(); break; } }); - public void RefreshScores() => Scheduler.AddOnce(UpdateScores); + public void RefetchScores() => Scheduler.AddOnce(refetchScores); - protected void UpdateScores() + private void refetchScores() { // don't display any scores or placeholder until the first Scores_Set has been called. // this avoids scope changes flickering a "no scores" placeholder before initialisation of song select is finished. diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index 134e083c42..e4ec3ac4a5 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components return; Scores = null; - UpdateScores(); + RefetchScores(); }, true); } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 4114a5e9a0..542851cb0f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -220,7 +220,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override Screen CreateGameplayScreen() => new PlayerLoader(() => new PlaylistsPlayer(Room, SelectedItem.Value) { - Exited = () => leaderboard.RefreshScores() + Exited = () => leaderboard.RefetchScores() }); } } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 31cbe91f5c..02e4e162dd 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Select.Leaderboards Scores = null; if (IsOnlineScope) - UpdateScores(); + RefetchScores(); else { if (IsLoaded) @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Select.Leaderboards filterMods = value; - UpdateScores(); + RefetchScores(); } } @@ -96,11 +96,11 @@ namespace osu.Game.Screens.Select.Leaderboards [BackgroundDependencyLoader] private void load() { - ruleset.ValueChanged += _ => UpdateScores(); + ruleset.ValueChanged += _ => RefetchScores(); mods.ValueChanged += _ => { if (filterMods) - UpdateScores(); + RefetchScores(); }; } @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Select.Leaderboards (_, changes, ___) => { if (!IsOnlineScope) - RefreshScores(); + RefetchScores(); }); } diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index b8b8e3e4bc..09f75b7658 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Select { base.Refresh(); - Leaderboard.RefreshScores(); + Leaderboard.RefetchScores(); } protected override void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods) From b9dac6c3b2d312bc79fefff4d5abb1c24d78c00c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 21:33:22 +0900 Subject: [PATCH 262/587] Reorder and tidy up bindable flows --- osu.Game/Online/Leaderboards/Leaderboard.cs | 81 ++++++++++++--------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index cd67fc7301..af448b7d7b 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -46,6 +46,18 @@ namespace osu.Game.Online.Leaderboards private bool scoresLoadedOnce; + private APIRequest getScoresRequest; + private ScheduledDelegate getScoresRequestCallback; + + protected abstract bool IsOnlineScope { get; } + + [Resolved(CanBeNull = true)] + private IAPIProvider api { get; set; } + + private ScheduledDelegate pendingUpdateScores; + + private readonly IBindable apiState = new Bindable(); + private readonly Container content; protected override Container Content => content; @@ -239,44 +251,34 @@ namespace osu.Game.Online.Leaderboards protected virtual void Reset() { - getScoresRequest?.Cancel(); - getScoresRequest = null; + cancelPendingWork(); + Scores = null; } - [Resolved(CanBeNull = true)] - private IAPIProvider api { get; set; } - - private ScheduledDelegate pendingUpdateScores; - - private readonly IBindable apiState = new Bindable(); - - [BackgroundDependencyLoader] - private void load() + protected override void LoadComplete() { + base.LoadComplete(); + if (api != null) - apiState.BindTo(api.State); - - apiState.BindValueChanged(onlineStateChanged, true); - } - - private APIRequest getScoresRequest; - private ScheduledDelegate getScoresRequestCallback; - - protected abstract bool IsOnlineScope { get; } - - private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => - { - switch (state.NewValue) { - case APIState.Online: - case APIState.Offline: - if (IsOnlineScope) - RefetchScores(); + apiState.BindTo(api.State); + apiState.BindValueChanged(state => + { + switch (state.NewValue) + { + case APIState.Online: + case APIState.Offline: + if (IsOnlineScope) + RefetchScores(); - break; + break; + } + }); } - }); + + RefetchScores(); + } public void RefetchScores() => Scheduler.AddOnce(refetchScores); @@ -286,13 +288,8 @@ namespace osu.Game.Online.Leaderboards // this avoids scope changes flickering a "no scores" placeholder before initialisation of song select is finished. if (!scoresLoadedOnce) return; - getScoresRequest?.Cancel(); - getScoresRequest = null; + cancelPendingWork(); - getScoresRequestCallback?.Cancel(); - getScoresRequestCallback = null; - - pendingUpdateScores?.Cancel(); pendingUpdateScores = Schedule(() => { PlaceholderState = PlaceholderState.Retrieving; @@ -319,6 +316,18 @@ namespace osu.Game.Online.Leaderboards }); } + private void cancelPendingWork() + { + getScoresRequest?.Cancel(); + getScoresRequest = null; + + getScoresRequestCallback?.Cancel(); + getScoresRequestCallback = null; + + pendingUpdateScores?.Cancel(); + pendingUpdateScores = null; + } + /// /// Performs a fetch/refresh of scores to be displayed. /// From 64925b3feaafa353bbfb87af39e13ee9e762a8bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 21:38:23 +0900 Subject: [PATCH 263/587] Remove unused `Content` override --- osu.Game/Online/Leaderboards/Leaderboard.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index af448b7d7b..785c2e2ecf 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -29,7 +29,7 @@ namespace osu.Game.Online.Leaderboards /// /// The scope of the leaderboard (ie. global or local). /// The score model class. - public abstract class Leaderboard : Container + public abstract class Leaderboard : CompositeDrawable { private const double fade_duration = 300; @@ -58,10 +58,6 @@ namespace osu.Game.Online.Leaderboards private readonly IBindable apiState = new Bindable(); - private readonly Container content; - - protected override Container Content => content; - private ICollection scores; public ICollection Scores @@ -231,7 +227,7 @@ namespace osu.Game.Online.Leaderboards }, new Drawable[] { - content = new Container + new Container { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, From 17aa9f304000505d5aff1d04239f5fa828873871 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 21:47:28 +0900 Subject: [PATCH 264/587] Remove pointless level of schedule/cancel logic --- osu.Game/Online/Leaderboards/Leaderboard.cs | 39 ++++++++------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 785c2e2ecf..7ab3d0343a 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -54,8 +54,6 @@ namespace osu.Game.Online.Leaderboards [Resolved(CanBeNull = true)] private IAPIProvider api { get; set; } - private ScheduledDelegate pendingUpdateScores; - private readonly IBindable apiState = new Bindable(); private ICollection scores; @@ -248,7 +246,6 @@ namespace osu.Game.Online.Leaderboards protected virtual void Reset() { cancelPendingWork(); - Scores = null; } @@ -286,30 +283,27 @@ namespace osu.Game.Online.Leaderboards cancelPendingWork(); - pendingUpdateScores = Schedule(() => + PlaceholderState = PlaceholderState.Retrieving; + loading.Show(); + + getScoresRequest = FetchScores(scores => getScoresRequestCallback = Schedule(() => { - PlaceholderState = PlaceholderState.Retrieving; - loading.Show(); + Scores = scores.ToArray(); + PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; + })); - getScoresRequest = FetchScores(scores => getScoresRequestCallback = Schedule(() => - { - Scores = scores.ToArray(); - PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; - })); + if (getScoresRequest == null) + return; - if (getScoresRequest == null) + getScoresRequest.Failure += e => getScoresRequestCallback = Schedule(() => + { + if (e is OperationCanceledException) return; - getScoresRequest.Failure += e => getScoresRequestCallback = Schedule(() => - { - if (e is OperationCanceledException) - return; - - PlaceholderState = PlaceholderState.NetworkFailure; - }); - - api?.Queue(getScoresRequest); + PlaceholderState = PlaceholderState.NetworkFailure; }); + + api?.Queue(getScoresRequest); } private void cancelPendingWork() @@ -319,9 +313,6 @@ namespace osu.Game.Online.Leaderboards getScoresRequestCallback?.Cancel(); getScoresRequestCallback = null; - - pendingUpdateScores?.Cancel(); - pendingUpdateScores = null; } /// From c5486586623acd425be534fced770e456493c85c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 21:49:52 +0900 Subject: [PATCH 265/587] Remove move unused pieces --- osu.Game/Online/Leaderboards/Leaderboard.cs | 29 ++++++++------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 7ab3d0343a..a426d2b448 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -82,7 +82,13 @@ namespace osu.Game.Online.Leaderboards // ensure placeholder is hidden when displaying scores PlaceholderState = PlaceholderState.Successful; - var scoreFlow = CreateScoreFlow(); + var scoreFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 5f), + Padding = new MarginPadding { Top = 10, Bottom = 5 }, + }; scoreFlow.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)); // schedule because we may not be loaded yet (LoadComponentAsync complains). @@ -118,15 +124,6 @@ namespace osu.Game.Online.Leaderboards } } - protected virtual FillFlowContainer CreateScoreFlow() - => new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 5f), - Padding = new MarginPadding { Top = 10, Bottom = 5 }, - }; - private TScope scope; public TScope Scope @@ -345,9 +342,6 @@ namespace osu.Game.Online.Leaderboards currentPlaceholder = placeholder; } - protected virtual bool FadeBottom => true; - protected virtual bool FadeTop => false; - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -366,22 +360,21 @@ namespace osu.Game.Online.Leaderboards float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scrollFlow).Y; float bottomY = topY + LeaderboardScore.HEIGHT; - bool requireTopFade = FadeTop && topY <= fadeTop; - bool requireBottomFade = FadeBottom && bottomY >= fadeBottom; + bool requireBottomFade = bottomY >= fadeBottom; - if (!requireTopFade && !requireBottomFade) + if (!requireBottomFade) c.Colour = Color4.White; else if (topY > fadeBottom + LeaderboardScore.HEIGHT || bottomY < fadeTop - LeaderboardScore.HEIGHT) c.Colour = Color4.Transparent; else { - if (bottomY - fadeBottom > 0 && FadeBottom) + if (bottomY - fadeBottom > 0) { c.Colour = ColourInfo.GradientVertical( Color4.White.Opacity(Math.Min(1 - (topY - fadeBottom) / LeaderboardScore.HEIGHT, 1)), Color4.White.Opacity(Math.Min(1 - (bottomY - fadeBottom) / LeaderboardScore.HEIGHT, 1))); } - else if (FadeTop) + else { c.Colour = ColourInfo.GradientVertical( Color4.White.Opacity(Math.Min(1 - (fadeTop - topY) / LeaderboardScore.HEIGHT, 1)), From b85b2c01fb248db8a3deb773ba1ed5da930d07c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 21:53:12 +0900 Subject: [PATCH 266/587] Reorder based on accessibility and add regions --- osu.Game/Online/Leaderboards/Leaderboard.cs | 268 ++++++++++---------- 1 file changed, 138 insertions(+), 130 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index a426d2b448..d843dcb2bb 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -139,6 +139,139 @@ namespace osu.Game.Online.Leaderboards } } + protected Leaderboard() + { + InternalChildren = new Drawable[] + { + new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + scrollContainer = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + } + }, + new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Child = topScoreContainer = new UserTopScoreContainer(CreateDrawableTopScore) + }, + }, + }, + }, + }, + loading = new LoadingSpinner(), + placeholderContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (api != null) + { + apiState.BindTo(api.State); + apiState.BindValueChanged(state => + { + switch (state.NewValue) + { + case APIState.Online: + case APIState.Offline: + if (IsOnlineScope) + RefetchScores(); + + break; + } + }); + } + + RefetchScores(); + } + + public void RefetchScores() => Scheduler.AddOnce(refetchScores); + + protected virtual void Reset() + { + cancelPendingWork(); + Scores = null; + } + + /// + /// Performs a fetch/refresh of scores to be displayed. + /// + /// A callback which should be called when fetching is completed. Scheduling is not required. + /// An responsible for the fetch operation. This will be queued and performed automatically. + protected abstract APIRequest FetchScores(Action> scoresCallback); + + protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); + + protected abstract LeaderboardScore CreateDrawableTopScore(TScoreInfo model); + + private void refetchScores() + { + // don't display any scores or placeholder until the first Scores_Set has been called. + // this avoids scope changes flickering a "no scores" placeholder before initialisation of song select is finished. + if (!scoresLoadedOnce) return; + + cancelPendingWork(); + + PlaceholderState = PlaceholderState.Retrieving; + loading.Show(); + + getScoresRequest = FetchScores(scores => getScoresRequestCallback = Schedule(() => + { + Scores = scores.ToArray(); + PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; + })); + + if (getScoresRequest == null) + return; + + getScoresRequest.Failure += e => getScoresRequestCallback = Schedule(() => + { + if (e is OperationCanceledException) + return; + + PlaceholderState = PlaceholderState.NetworkFailure; + }); + + api?.Queue(getScoresRequest); + } + + private void cancelPendingWork() + { + getScoresRequest?.Cancel(); + getScoresRequest = null; + + getScoresRequestCallback?.Cancel(); + getScoresRequestCallback = null; + } + + #region Placeholder handling + + private Placeholder currentPlaceholder; + private PlaceholderState placeholderState; /// @@ -194,133 +327,6 @@ namespace osu.Game.Online.Leaderboards } } - protected Leaderboard() - { - InternalChildren = new Drawable[] - { - new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] - { - scrollContainer = new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - } - }, - new Drawable[] - { - new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Child = topScoreContainer = new UserTopScoreContainer(CreateDrawableTopScore) - }, - }, - }, - }, - }, - loading = new LoadingSpinner(), - placeholderContainer = new Container - { - RelativeSizeAxes = Axes.Both - }, - }; - } - - protected virtual void Reset() - { - cancelPendingWork(); - Scores = null; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - if (api != null) - { - apiState.BindTo(api.State); - apiState.BindValueChanged(state => - { - switch (state.NewValue) - { - case APIState.Online: - case APIState.Offline: - if (IsOnlineScope) - RefetchScores(); - - break; - } - }); - } - - RefetchScores(); - } - - public void RefetchScores() => Scheduler.AddOnce(refetchScores); - - private void refetchScores() - { - // don't display any scores or placeholder until the first Scores_Set has been called. - // this avoids scope changes flickering a "no scores" placeholder before initialisation of song select is finished. - if (!scoresLoadedOnce) return; - - cancelPendingWork(); - - PlaceholderState = PlaceholderState.Retrieving; - loading.Show(); - - getScoresRequest = FetchScores(scores => getScoresRequestCallback = Schedule(() => - { - Scores = scores.ToArray(); - PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; - })); - - if (getScoresRequest == null) - return; - - getScoresRequest.Failure += e => getScoresRequestCallback = Schedule(() => - { - if (e is OperationCanceledException) - return; - - PlaceholderState = PlaceholderState.NetworkFailure; - }); - - api?.Queue(getScoresRequest); - } - - private void cancelPendingWork() - { - getScoresRequest?.Cancel(); - getScoresRequest = null; - - getScoresRequestCallback?.Cancel(); - getScoresRequestCallback = null; - } - - /// - /// Performs a fetch/refresh of scores to be displayed. - /// - /// A callback which should be called when fetching is completed. Scheduling is not required. - /// An responsible for the fetch operation. This will be queued and performed automatically. - protected abstract APIRequest FetchScores(Action> scoresCallback); - - private Placeholder currentPlaceholder; - private void replacePlaceholder(Placeholder placeholder) { if (placeholder != null && placeholder.Equals(currentPlaceholder)) @@ -342,6 +348,10 @@ namespace osu.Game.Online.Leaderboards currentPlaceholder = placeholder; } + #endregion + + #region Fade handling + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -384,8 +394,6 @@ namespace osu.Game.Online.Leaderboards } } - protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); - - protected abstract LeaderboardScore CreateDrawableTopScore(TScoreInfo model); + #endregion } } From 661fec7c8abd1fba873a5003092d9f5f45da475c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 21:59:29 +0900 Subject: [PATCH 267/587] Make score setter private --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 5 ++++- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 3 --- osu.Game/Online/Leaderboards/Leaderboard.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index e31be1d51a..7ffab2eebc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -100,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestGlobalScoresDisplay() { AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); - AddStep(@"New Scores", () => leaderboard.Scores = generateSampleScores(null)); + AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(null))); } [Test] @@ -422,6 +423,8 @@ namespace osu.Game.Tests.Visual.SongSelect { PlaceholderState = state; } + + public void SetScores(ICollection scores) => Scores = scores; } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 4938fd1271..985ff6f251 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -128,9 +128,6 @@ namespace osu.Game.Tests.Visual.UserInterface scoreManager.Undelete(r.All().Where(s => s.DeletePending).ToList()); }); - leaderboard.Scores = null; - leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables - leaderboard.BeatmapInfo = beatmapInfo; leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed }); diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index d843dcb2bb..0c58faa178 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -61,7 +61,7 @@ namespace osu.Game.Online.Leaderboards public ICollection Scores { get => scores; - set + protected set { scores = value; From a700ad38499ed2fbaed093e5de5570b27f5cd0c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 22:04:34 +0900 Subject: [PATCH 268/587] Remove `scoresLoadedOnce` weirdness --- osu.Game/Online/Leaderboards/Leaderboard.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 0c58faa178..238e883d24 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -44,8 +44,6 @@ namespace osu.Game.Online.Leaderboards private ScheduledDelegate showScoresDelegate; private CancellationTokenSource showScoresCancellationSource; - private bool scoresLoadedOnce; - private APIRequest getScoresRequest; private ScheduledDelegate getScoresRequestCallback; @@ -65,8 +63,6 @@ namespace osu.Game.Online.Leaderboards { scores = value; - scoresLoadedOnce = true; - scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); scrollFlow = null; @@ -230,10 +226,6 @@ namespace osu.Game.Online.Leaderboards private void refetchScores() { - // don't display any scores or placeholder until the first Scores_Set has been called. - // this avoids scope changes flickering a "no scores" placeholder before initialisation of song select is finished. - if (!scoresLoadedOnce) return; - cancelPendingWork(); PlaceholderState = PlaceholderState.Retrieving; From c48e9f2bbdc2dba7360e5d0b7ef9e4dad12256e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 22:13:52 +0900 Subject: [PATCH 269/587] Remove more unnecessary schedule/cancel logic --- osu.Game/Online/Leaderboards/Leaderboard.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 238e883d24..e8fc2d126e 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -41,7 +43,6 @@ namespace osu.Game.Online.Leaderboards private readonly LoadingSpinner loading; - private ScheduledDelegate showScoresDelegate; private CancellationTokenSource showScoresCancellationSource; private APIRequest getScoresRequest; @@ -61,15 +62,16 @@ namespace osu.Game.Online.Leaderboards get => scores; protected set { + Debug.Assert(ThreadSafety.IsUpdateThread); + scores = value; scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); scrollFlow = null; - showScoresDelegate?.Cancel(); showScoresCancellationSource?.Cancel(); - if (scores == null || !scores.Any()) + if (scores?.Any() != true) { loading.Hide(); return; @@ -84,11 +86,11 @@ namespace osu.Game.Online.Leaderboards AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 5f), Padding = new MarginPadding { Top = 10, Bottom = 5 }, + ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)) }; - scoreFlow.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)); // schedule because we may not be loaded yet (LoadComponentAsync complains). - showScoresDelegate = Schedule(() => LoadComponentAsync(scoreFlow, _ => + LoadComponentAsync(scoreFlow, _ => { scrollContainer.Add(scrollFlow = scoreFlow); @@ -100,9 +102,9 @@ namespace osu.Game.Online.Leaderboards s.Show(); } - scrollContainer.ScrollTo(0f, false); + scrollContainer.ScrollToStart(false); loading.Hide(); - }, (showScoresCancellationSource = new CancellationTokenSource()).Token)); + }, (showScoresCancellationSource = new CancellationTokenSource()).Token); } } @@ -253,6 +255,9 @@ namespace osu.Game.Online.Leaderboards private void cancelPendingWork() { + showScoresCancellationSource?.Cancel(); + showScoresCancellationSource = null; + getScoresRequest?.Cancel(); getScoresRequest = null; From 13f445ddd5c49b2be0ccf2efeeff72c4703b1b8b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 22:28:13 +0900 Subject: [PATCH 270/587] Move score update code into own method --- osu.Game/Online/Leaderboards/Leaderboard.cs | 91 +++++++++++---------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index e8fc2d126e..0302568b34 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -33,6 +33,8 @@ namespace osu.Game.Online.Leaderboards /// The score model class. public abstract class Leaderboard : CompositeDrawable { + protected abstract bool IsOnlineScope { get; } + private const double fade_duration = 300; private readonly OsuScrollContainer scrollContainer; @@ -48,8 +50,6 @@ namespace osu.Game.Online.Leaderboards private APIRequest getScoresRequest; private ScheduledDelegate getScoresRequestCallback; - protected abstract bool IsOnlineScope { get; } - [Resolved(CanBeNull = true)] private IAPIProvider api { get; set; } @@ -62,49 +62,9 @@ namespace osu.Game.Online.Leaderboards get => scores; protected set { - Debug.Assert(ThreadSafety.IsUpdateThread); - scores = value; - scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); - scrollFlow = null; - - showScoresCancellationSource?.Cancel(); - - if (scores?.Any() != true) - { - loading.Hide(); - return; - } - - // ensure placeholder is hidden when displaying scores - PlaceholderState = PlaceholderState.Successful; - - var scoreFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 5f), - Padding = new MarginPadding { Top = 10, Bottom = 5 }, - ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)) - }; - - // schedule because we may not be loaded yet (LoadComponentAsync complains). - LoadComponentAsync(scoreFlow, _ => - { - scrollContainer.Add(scrollFlow = scoreFlow); - - int i = 0; - - foreach (var s in scrollFlow.Children) - { - using (s.BeginDelayedSequence(i++ * 50)) - s.Show(); - } - - scrollContainer.ScrollToStart(false); - loading.Hide(); - }, (showScoresCancellationSource = new CancellationTokenSource()).Token); + updateScoresDrawables(); } } @@ -324,6 +284,51 @@ namespace osu.Game.Online.Leaderboards } } + private void updateScoresDrawables() + { + Debug.Assert(ThreadSafety.IsUpdateThread); + + scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); + scrollFlow = null; + + showScoresCancellationSource?.Cancel(); + + if (scores?.Any() != true) + { + loading.Hide(); + return; + } + + // ensure placeholder is hidden when displaying scores + PlaceholderState = PlaceholderState.Successful; + + var scoreFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 5f), + Padding = new MarginPadding { Top = 10, Bottom = 5 }, + ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)) + }; + + // schedule because we may not be loaded yet (LoadComponentAsync complains). + LoadComponentAsync(scoreFlow, _ => + { + scrollContainer.Add(scrollFlow = scoreFlow); + + int i = 0; + + foreach (var s in scrollFlow.Children) + { + using (s.BeginDelayedSequence(i++ * 50)) + s.Show(); + } + + scrollContainer.ScrollToStart(false); + loading.Hide(); + }, (showScoresCancellationSource = new CancellationTokenSource()).Token); + } + private void replacePlaceholder(Placeholder placeholder) { if (placeholder != null && placeholder.Equals(currentPlaceholder)) From 3d59bab7c61cea8d71b362bd21f9bc183c3baf7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 22:47:45 +0900 Subject: [PATCH 271/587] Remove fetch callback logic completely --- osu.Game/Online/Leaderboards/Leaderboard.cs | 25 +--- .../Match/Components/MatchLeaderboard.cs | 6 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 130 ++++++++---------- 3 files changed, 67 insertions(+), 94 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 0302568b34..eeb814b4cb 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -3,12 +3,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Development; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -178,9 +177,9 @@ namespace osu.Game.Online.Leaderboards /// /// Performs a fetch/refresh of scores to be displayed. /// - /// A callback which should be called when fetching is completed. Scheduling is not required. /// An responsible for the fetch operation. This will be queued and performed automatically. - protected abstract APIRequest FetchScores(Action> scoresCallback); + [CanBeNull] + protected abstract APIRequest FetchScores(); protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); @@ -193,11 +192,7 @@ namespace osu.Game.Online.Leaderboards PlaceholderState = PlaceholderState.Retrieving; loading.Show(); - getScoresRequest = FetchScores(scores => getScoresRequestCallback = Schedule(() => - { - Scores = scores.ToArray(); - PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; - })); + getScoresRequest = FetchScores(); if (getScoresRequest == null) return; @@ -240,11 +235,6 @@ namespace osu.Game.Online.Leaderboards get => placeholderState; set { - if (value != PlaceholderState.Successful) - { - Reset(); - } - if (value == placeholderState) return; @@ -284,10 +274,8 @@ namespace osu.Game.Online.Leaderboards } } - private void updateScoresDrawables() + private void updateScoresDrawables() => Scheduler.Add(() => { - Debug.Assert(ThreadSafety.IsUpdateThread); - scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); scrollFlow = null; @@ -296,6 +284,7 @@ namespace osu.Game.Online.Leaderboards if (scores?.Any() != true) { loading.Hide(); + PlaceholderState = PlaceholderState.NoScores; return; } @@ -327,7 +316,7 @@ namespace osu.Game.Online.Leaderboards scrollContainer.ScrollToStart(false); loading.Hide(); }, (showScoresCancellationSource = new CancellationTokenSource()).Token); - } + }, false); private void replacePlaceholder(Placeholder placeholder) { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index e4ec3ac4a5..5b60f64160 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; @@ -32,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected override bool IsOnlineScope => true; - protected override APIRequest FetchScores(Action> scoresCallback) + protected override APIRequest FetchScores() { if (roomId.Value == null) return null; @@ -41,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components req.Success += r => { - scoresCallback?.Invoke(r.Leaderboard); + Scores = r.Leaderboard; TopScore = r.UserScore; }; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 02e4e162dd..898b894541 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -25,12 +25,6 @@ namespace osu.Game.Screens.Select.Leaderboards { public Action ScoreSelected; - [Resolved] - private RulesetStore rulesets { get; set; } - - [Resolved] - private RealmAccess realm { get; set; } - private BeatmapInfo beatmapInfo; public BeatmapInfo BeatmapInfo @@ -52,13 +46,7 @@ namespace osu.Game.Screens.Select.Leaderboards beatmapInfo = value; Scores = null; - if (IsOnlineScope) - RefetchScores(); - else - { - if (IsLoaded) - refreshRealmSubscription(); - } + RefetchScores(); } } @@ -93,6 +81,14 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private RulesetStore rulesets { get; set; } + + [Resolved] + private RealmAccess realm { get; set; } + + private IDisposable scoreSubscription; + [BackgroundDependencyLoader] private void load() { @@ -104,33 +100,6 @@ namespace osu.Game.Screens.Select.Leaderboards }; } - protected override void LoadComplete() - { - base.LoadComplete(); - - refreshRealmSubscription(); - } - - private IDisposable scoreSubscription; - - private void refreshRealmSubscription() - { - scoreSubscription?.Dispose(); - scoreSubscription = null; - - if (beatmapInfo == null) - return; - - scoreSubscription = realm.RegisterForNotifications(r => - r.All() - .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID), - (_, changes, ___) => - { - if (!IsOnlineScope) - RefetchScores(); - }); - } - protected override void Reset() { base.Reset(); @@ -141,13 +110,11 @@ namespace osu.Game.Screens.Select.Leaderboards private CancellationTokenSource loadCancellationSource; - protected override APIRequest FetchScores(Action> scoresCallback) + protected override APIRequest FetchScores() { loadCancellationSource?.Cancel(); loadCancellationSource = new CancellationTokenSource(); - var cancellationToken = loadCancellationSource.Token; - var fetchBeatmapInfo = BeatmapInfo; if (fetchBeatmapInfo == null) @@ -158,31 +125,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { - realm.Run(r => - { - var scores = r.All() - .AsEnumerable() - // TODO: update to use a realm filter directly (or at least figure out the beatmap part to reduce scope). - .Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.ShortName == ruleset.Value.ShortName); - - if (filterMods && !mods.Value.Any()) - { - // we need to filter out all scores that have any mods to get all local nomod scores - scores = scores.Where(s => !s.Mods.Any()); - } - else if (filterMods) - { - // otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters) - // we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself - var selectedMods = mods.Value.Select(m => m.Acronym); - scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); - } - - scores = scores.Detach(); - - scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) - .ContinueWith(ordered => scoresCallback?.Invoke(ordered.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); - }); + subscribeToLocalScores(); return null; } @@ -217,13 +160,13 @@ namespace osu.Game.Screens.Select.Leaderboards req.Success += r => { - scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets, fetchBeatmapInfo)).ToArray(), cancellationToken) + scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets, fetchBeatmapInfo)).ToArray(), loadCancellationSource.Token) .ContinueWith(task => Schedule(() => { - if (cancellationToken.IsCancellationRequested) + if (loadCancellationSource.IsCancellationRequested) return; - scoresCallback?.Invoke(task.GetResultSafely()); + Scores = task.GetResultSafely(); TopScore = r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo); }), TaskContinuationOptions.OnlyOnRanToCompletion); }; @@ -241,10 +184,53 @@ namespace osu.Game.Screens.Select.Leaderboards Action = () => ScoreSelected?.Invoke(model) }; + private void subscribeToLocalScores() + { + scoreSubscription?.Dispose(); + scoreSubscription = null; + + if (beatmapInfo == null) + return; + + scoreSubscription = realm.RegisterForNotifications(r => + r.All().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" + + $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" + + $" AND {nameof(ScoreInfo.DeletePending)} == false" + , beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); + } + + private void localScoresChanged(IRealmCollection sender, ChangeSet changes, Exception exception) + { + if (IsOnlineScope) + return; + + var scores = sender.AsEnumerable(); + + if (filterMods && !mods.Value.Any()) + { + // we need to filter out all scores that have any mods to get all local nomod scores + scores = scores.Where(s => !s.Mods.Any()); + } + else if (filterMods) + { + // otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters) + // we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself + var selectedMods = mods.Value.Select(m => m.Acronym); + scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); + } + + scores = scores.Detach(); + + scoreManager.OrderByTotalScoreAsync(scores.ToArray(), loadCancellationSource.Token) + .ContinueWith(ordered => + { + Scores = ordered.GetResultSafely(); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - scoreSubscription?.Dispose(); } } From daea13f49158b5c464f20c4b395a85bd46206991 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 23:14:26 +0900 Subject: [PATCH 272/587] Simplify flow of cancellation token --- osu.Game/Online/Leaderboards/Leaderboard.cs | 42 ++++++------ .../Match/Components/MatchLeaderboard.cs | 6 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 65 +++++++++---------- 3 files changed, 55 insertions(+), 58 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index eeb814b4cb..0d430f4903 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using JetBrains.Annotations; @@ -13,7 +14,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Threading; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; @@ -44,10 +44,9 @@ namespace osu.Game.Online.Leaderboards private readonly LoadingSpinner loading; - private CancellationTokenSource showScoresCancellationSource; + private CancellationTokenSource currentFetchCancellationSource; - private APIRequest getScoresRequest; - private ScheduledDelegate getScoresRequestCallback; + private APIRequest fetchScoresRequest; [Resolved(CanBeNull = true)] private IAPIProvider api { get; set; } @@ -62,7 +61,6 @@ namespace osu.Game.Online.Leaderboards protected set { scores = value; - updateScoresDrawables(); } } @@ -177,9 +175,10 @@ namespace osu.Game.Online.Leaderboards /// /// Performs a fetch/refresh of scores to be displayed. /// + /// /// An responsible for the fetch operation. This will be queued and performed automatically. [CanBeNull] - protected abstract APIRequest FetchScores(); + protected abstract APIRequest FetchScores(CancellationToken cancellationToken); protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); @@ -187,37 +186,36 @@ namespace osu.Game.Online.Leaderboards private void refetchScores() { - cancelPendingWork(); + Reset(); PlaceholderState = PlaceholderState.Retrieving; loading.Show(); - getScoresRequest = FetchScores(); + currentFetchCancellationSource = new CancellationTokenSource(); - if (getScoresRequest == null) + fetchScoresRequest = FetchScores(currentFetchCancellationSource.Token); + + if (fetchScoresRequest == null) return; - getScoresRequest.Failure += e => getScoresRequestCallback = Schedule(() => + fetchScoresRequest.Failure += e => Schedule(() => { - if (e is OperationCanceledException) + if (e is OperationCanceledException || currentFetchCancellationSource.IsCancellationRequested) return; PlaceholderState = PlaceholderState.NetworkFailure; }); - api?.Queue(getScoresRequest); + api?.Queue(fetchScoresRequest); } private void cancelPendingWork() { - showScoresCancellationSource?.Cancel(); - showScoresCancellationSource = null; + currentFetchCancellationSource?.Cancel(); + currentFetchCancellationSource = null; - getScoresRequest?.Cancel(); - getScoresRequest = null; - - getScoresRequestCallback?.Cancel(); - getScoresRequestCallback = null; + fetchScoresRequest?.Cancel(); + fetchScoresRequest = null; } #region Placeholder handling @@ -279,8 +277,6 @@ namespace osu.Game.Online.Leaderboards scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); scrollFlow = null; - showScoresCancellationSource?.Cancel(); - if (scores?.Any() != true) { loading.Hide(); @@ -288,6 +284,8 @@ namespace osu.Game.Online.Leaderboards return; } + Debug.Assert(!currentFetchCancellationSource.IsCancellationRequested); + // ensure placeholder is hidden when displaying scores PlaceholderState = PlaceholderState.Successful; @@ -315,7 +313,7 @@ namespace osu.Game.Online.Leaderboards scrollContainer.ScrollToStart(false); loading.Hide(); - }, (showScoresCancellationSource = new CancellationTokenSource()).Token); + }, currentFetchCancellationSource.Token); }, false); private void replacePlaceholder(Placeholder placeholder) diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index 5b60f64160..1945899a11 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.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.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; @@ -30,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected override bool IsOnlineScope => true; - protected override APIRequest FetchScores() + protected override APIRequest FetchScores(CancellationToken cancellationToken) { if (roomId.Value == null) return null; @@ -39,6 +40,9 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components req.Success += r => { + if (cancellationToken.IsCancellationRequested) + return; + Scores = r.Leaderboard; TopScore = r.UserScore; }; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 898b894541..388395c9f9 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -108,13 +108,8 @@ namespace osu.Game.Screens.Select.Leaderboards protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; - private CancellationTokenSource loadCancellationSource; - - protected override APIRequest FetchScores() + protected override APIRequest FetchScores(CancellationToken cancellationToken) { - loadCancellationSource?.Cancel(); - loadCancellationSource = new CancellationTokenSource(); - var fetchBeatmapInfo = BeatmapInfo; if (fetchBeatmapInfo == null) @@ -125,7 +120,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { - subscribeToLocalScores(); + subscribeToLocalScores(cancellationToken); return null; } @@ -160,10 +155,10 @@ namespace osu.Game.Screens.Select.Leaderboards req.Success += r => { - scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets, fetchBeatmapInfo)).ToArray(), loadCancellationSource.Token) + scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets, fetchBeatmapInfo)).ToArray(), cancellationToken) .ContinueWith(task => Schedule(() => { - if (loadCancellationSource.IsCancellationRequested) + if (cancellationToken.IsCancellationRequested) return; Scores = task.GetResultSafely(); @@ -184,7 +179,7 @@ namespace osu.Game.Screens.Select.Leaderboards Action = () => ScoreSelected?.Invoke(model) }; - private void subscribeToLocalScores() + private void subscribeToLocalScores(CancellationToken cancellationToken) { scoreSubscription?.Dispose(); scoreSubscription = null; @@ -197,35 +192,35 @@ namespace osu.Game.Screens.Select.Leaderboards + $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" + $" AND {nameof(ScoreInfo.DeletePending)} == false" , beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); - } - private void localScoresChanged(IRealmCollection sender, ChangeSet changes, Exception exception) - { - if (IsOnlineScope) - return; - - var scores = sender.AsEnumerable(); - - if (filterMods && !mods.Value.Any()) + void localScoresChanged(IRealmCollection sender, ChangeSet changes, Exception exception) { - // we need to filter out all scores that have any mods to get all local nomod scores - scores = scores.Where(s => !s.Mods.Any()); - } - else if (filterMods) - { - // otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters) - // we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself - var selectedMods = mods.Value.Select(m => m.Acronym); - scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); - } + if (IsOnlineScope || cancellationToken.IsCancellationRequested) + return; - scores = scores.Detach(); + var scores = sender.AsEnumerable(); - scoreManager.OrderByTotalScoreAsync(scores.ToArray(), loadCancellationSource.Token) - .ContinueWith(ordered => - { - Scores = ordered.GetResultSafely(); - }, TaskContinuationOptions.OnlyOnRanToCompletion); + if (filterMods && !mods.Value.Any()) + { + // we need to filter out all scores that have any mods to get all local nomod scores + scores = scores.Where(s => !s.Mods.Any()); + } + else if (filterMods) + { + // otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters) + // we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself + var selectedMods = mods.Value.Select(m => m.Acronym); + scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); + } + + scores = scores.Detach(); + + scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) + .ContinueWith(ordered => + { + Scores = ordered.GetResultSafely(); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + } } protected override void Dispose(bool isDisposing) From 0293d95f829f7b3596e472244e42ed040ee62717 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jan 2022 23:17:06 +0900 Subject: [PATCH 273/587] Simplify `IsOnlineScope` usage --- osu.Game/Online/Leaderboards/Leaderboard.cs | 3 +++ osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 0d430f4903..6c2f0d9425 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -32,6 +32,9 @@ namespace osu.Game.Online.Leaderboards /// The score model class. public abstract class Leaderboard : CompositeDrawable { + /// + /// Whether the current scope should refetch in response to changes in API connectivity state. + /// protected abstract bool IsOnlineScope { get; } private const double fade_duration = 300; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 388395c9f9..e55bd56e62 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -195,7 +195,7 @@ namespace osu.Game.Screens.Select.Leaderboards void localScoresChanged(IRealmCollection sender, ChangeSet changes, Exception exception) { - if (IsOnlineScope || cancellationToken.IsCancellationRequested) + if (cancellationToken.IsCancellationRequested) return; var scores = sender.AsEnumerable(); From d0b74a91fba148d56fbd0ac586e5f6e1159e2a92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Jan 2022 02:12:36 +0900 Subject: [PATCH 274/587] Fix edge cases with score drawable loading --- osu.Game/Online/Leaderboards/Leaderboard.cs | 45 ++++++++++--------- .../Select/Leaderboards/BeatmapLeaderboard.cs | 1 - 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 6c2f0d9425..a1f07c469a 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using JetBrains.Annotations; @@ -43,11 +42,12 @@ namespace osu.Game.Online.Leaderboards private readonly Container placeholderContainer; private readonly UserTopScoreContainer topScoreContainer; - private FillFlowContainer scrollFlow; + private FillFlowContainer scoreFlowContainer; private readonly LoadingSpinner loading; private CancellationTokenSource currentFetchCancellationSource; + private CancellationTokenSource currentScoresAsyncLoadCancellationSource; private APIRequest fetchScoresRequest; @@ -64,7 +64,7 @@ namespace osu.Game.Online.Leaderboards protected set { scores = value; - updateScoresDrawables(); + Scheduler.AddOnce(updateScoresDrawables); } } @@ -239,6 +239,8 @@ namespace osu.Game.Online.Leaderboards if (value == placeholderState) return; + loading.Hide(); + switch (placeholderState = value) { case PlaceholderState.NetworkFailure: @@ -275,40 +277,41 @@ namespace osu.Game.Online.Leaderboards } } - private void updateScoresDrawables() => Scheduler.Add(() => + private void updateScoresDrawables() { - scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); - scrollFlow = null; + currentScoresAsyncLoadCancellationSource?.Cancel(); + currentScoresAsyncLoadCancellationSource = new CancellationTokenSource(); + + scoreFlowContainer? + .FadeOut(fade_duration, Easing.OutQuint) + .Expire(); + scoreFlowContainer = null; + + loading.Hide(); if (scores?.Any() != true) { - loading.Hide(); PlaceholderState = PlaceholderState.NoScores; return; } - Debug.Assert(!currentFetchCancellationSource.IsCancellationRequested); - // ensure placeholder is hidden when displaying scores PlaceholderState = PlaceholderState.Successful; - var scoreFlow = new FillFlowContainer + LoadComponentAsync(new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 5f), Padding = new MarginPadding { Top = 10, Bottom = 5 }, ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)) - }; - - // schedule because we may not be loaded yet (LoadComponentAsync complains). - LoadComponentAsync(scoreFlow, _ => + }, newFlow => { - scrollContainer.Add(scrollFlow = scoreFlow); + scrollContainer.Add(scoreFlowContainer = newFlow); int i = 0; - foreach (var s in scrollFlow.Children) + foreach (var s in scoreFlowContainer.Children) { using (s.BeginDelayedSequence(i++ * 50)) s.Show(); @@ -316,8 +319,8 @@ namespace osu.Game.Online.Leaderboards scrollContainer.ScrollToStart(false); loading.Hide(); - }, currentFetchCancellationSource.Token); - }, false); + }, currentScoresAsyncLoadCancellationSource.Token); + } private void replacePlaceholder(Placeholder placeholder) { @@ -354,12 +357,12 @@ namespace osu.Game.Online.Leaderboards if (!scrollContainer.IsScrolledToEnd()) fadeBottom -= LeaderboardScore.HEIGHT; - if (scrollFlow == null) + if (scoreFlowContainer == null) return; - foreach (var c in scrollFlow.Children) + foreach (var c in scoreFlowContainer.Children) { - float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scrollFlow).Y; + float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scoreFlowContainer).Y; float bottomY = topY + LeaderboardScore.HEIGHT; bool requireBottomFade = bottomY >= fadeBottom; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index e55bd56e62..31868624bb 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -121,7 +121,6 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { subscribeToLocalScores(cancellationToken); - return null; } From 6f54f8ad7897cc9bfa31e3ad00660eff3efeef59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Jan 2022 12:18:34 +0900 Subject: [PATCH 275/587] Add more safety around `CancellationToken` usage --- osu.Game/Online/Leaderboards/Leaderboard.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index a1f07c469a..cc6d7a7b62 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -189,6 +191,8 @@ namespace osu.Game.Online.Leaderboards private void refetchScores() { + Debug.Assert(ThreadSafety.IsUpdateThread); + Reset(); PlaceholderState = PlaceholderState.Retrieving; @@ -215,10 +219,8 @@ namespace osu.Game.Online.Leaderboards private void cancelPendingWork() { currentFetchCancellationSource?.Cancel(); - currentFetchCancellationSource = null; - + currentScoresAsyncLoadCancellationSource?.Cancel(); fetchScoresRequest?.Cancel(); - fetchScoresRequest = null; } #region Placeholder handling @@ -280,7 +282,6 @@ namespace osu.Game.Online.Leaderboards private void updateScoresDrawables() { currentScoresAsyncLoadCancellationSource?.Cancel(); - currentScoresAsyncLoadCancellationSource = new CancellationTokenSource(); scoreFlowContainer? .FadeOut(fade_duration, Easing.OutQuint) @@ -319,7 +320,7 @@ namespace osu.Game.Online.Leaderboards scrollContainer.ScrollToStart(false); loading.Hide(); - }, currentScoresAsyncLoadCancellationSource.Token); + }, (currentScoresAsyncLoadCancellationSource = new CancellationTokenSource()).Token); } private void replacePlaceholder(Placeholder placeholder) From a915b9cd3093bd2ede73c3d6ef9470ff12967dc1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Jan 2022 23:12:57 +0900 Subject: [PATCH 276/587] Fix occasional failures in `TestSceneDeleteLocalScore` --- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 985ff6f251..da4cf9c6e3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -135,11 +135,9 @@ namespace osu.Game.Tests.Visual.UserInterface [SetUpSteps] public void SetupSteps() { - // Ensure the leaderboard has finished async-loading drawables - AddUntilStep("wait for drawables", () => leaderboard.ChildrenOfType().Any()); - // Ensure the leaderboard items have finished showing up AddStep("finish transforms", () => leaderboard.FinishTransforms(true)); + AddUntilStep("wait for drawables", () => leaderboard.ChildrenOfType().Any()); } [Test] From 9861c50b33f8fa036e8bdf62ff1f646cab55c8ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 00:03:22 +0900 Subject: [PATCH 277/587] Remove pointless tests that no longer show anything valid --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 7ffab2eebc..b4b66e8afa 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -122,13 +122,6 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep(@"None selected", () => leaderboard.SetRetrievalState(PlaceholderState.NoneSelected)); } - [Test] - public void TestBeatmapStates() - { - foreach (BeatmapOnlineStatus status in Enum.GetValues(typeof(BeatmapOnlineStatus))) - AddStep($"{status} beatmap", () => showBeatmapWithStatus(status)); - } - private void showPersonalBestWithNullPosition() { leaderboard.TopScore = new ScoreInfo From e408d8ef0e1ced02cf3842c95318814e987c3303 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Sat, 29 Jan 2022 23:19:51 +0800 Subject: [PATCH 278/587] rename `Frames` to `ReplayFrames` --- .../Mods/TestSceneOsuModAlternate.cs | 8 ++++---- osu.Game/Tests/Visual/ModTestScene.cs | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs index 7b3eaa7ffd..de1f61a0bd 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods }, }, }, - Frames = new List + ReplayFrames = new List { new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton), new OsuReplayFrame(501, new Vector2(200)), @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods }, }, }, - Frames = new List + ReplayFrames = new List { new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), new OsuReplayFrame(501, new Vector2(100)), @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods }, }, }, - Frames = new List + ReplayFrames = new List { new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), new OsuReplayFrame(501, new Vector2(100)), @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } } }, - Frames = new List + ReplayFrames = new List { new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), new OsuReplayFrame(501, new Vector2(100)), diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 96fafa8727..2505864d59 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -72,14 +72,14 @@ namespace osu.Game.Tests.Visual protected override void PrepareReplay() { - if (currentTestData.Autoplay && currentTestData.Frames?.Count > 0) - throw new InvalidOperationException(@$"{nameof(ModTestData.Autoplay)} must be false when {nameof(ModTestData.Frames)} is specified."); + if (currentTestData.Autoplay && currentTestData.ReplayFrames?.Count > 0) + throw new InvalidOperationException(@$"{nameof(ModTestData.Autoplay)} must be false when {nameof(ModTestData.ReplayFrames)} is specified."); - if (currentTestData.Frames != null) + if (currentTestData.ReplayFrames != null) { DrawableRuleset?.SetReplayScore(new Score { - Replay = new Replay { Frames = currentTestData.Frames }, + Replay = new Replay { Frames = currentTestData.ReplayFrames }, ScoreInfo = new ScoreInfo { User = new APIUser { Username = @"Test" } }, }); } @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual /// The frames to use for replay. must be set to false. /// [CanBeNull] - public List Frames; + public List ReplayFrames; /// /// The beatmap for this test case. From 535216a0d3f5c063d5e5519ff4cd0533c3e72df5 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Sat, 29 Jan 2022 23:20:31 +0800 Subject: [PATCH 279/587] rename `CanIntercept` to `ShouldAlternate` --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index 9eeb060135..876742caba 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -25,11 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) }; public override ModType Type => ModType.DifficultyIncrease; public override IconUsage? Icon => FontAwesome.Solid.Keyboard; - - /// - /// Whether incoming input must be checked by before it is passed to gameplay. - /// - public bool CanIntercept => !isBreakTime.Value && introEnded; + public bool ShouldAlternate => !isBreakTime.Value && introEnded; private bool introEnded; private double earliestStartTime; @@ -86,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Mods } public bool OnPressed(KeyBindingPressEvent e) - => mod.CanIntercept && mod.onPressed(e.Action); + => mod.ShouldAlternate && mod.onPressed(e.Action); public void OnReleased(KeyBindingReleaseEvent e) { From 40f43344f16056d66d6713078ecc3b5ccf47372b Mon Sep 17 00:00:00 2001 From: Nitrous Date: Sat, 29 Jan 2022 23:31:26 +0800 Subject: [PATCH 280/587] remove unused using --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index 876742caba..db8bd217f6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; From 51acf79935724e5786ecd1b515f07657071b65fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 02:29:51 +0900 Subject: [PATCH 281/587] Change test exposure to property instead of method --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index b4b66e8afa..91ec1de3ad 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestGlobalScoresDisplay() { AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); - AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(null))); + AddStep(@"New Scores", () => leaderboard.Scores = generateSampleScores(null)); } [Test] @@ -417,7 +417,11 @@ namespace osu.Game.Tests.Visual.SongSelect PlaceholderState = state; } - public void SetScores(ICollection scores) => Scores = scores; + public new ICollection Scores + { + get => base.Scores; + set => base.Scores = value; + } } } } From 3d771c0fc7ed0f292ec28c0257ff80c122bcd058 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 02:34:31 +0900 Subject: [PATCH 282/587] Remove unnecessary `loading` hide call from `PlaceholderState_Set` and add more assertiveness --- osu.Game/Online/Leaderboards/Leaderboard.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index cc6d7a7b62..e20c8b5007 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -241,9 +241,11 @@ namespace osu.Game.Online.Leaderboards if (value == placeholderState) return; - loading.Hide(); + placeholderState = value; - switch (placeholderState = value) + Debug.Assert(placeholderState != PlaceholderState.Successful || scores?.Any() == true); + + switch (placeholderState) { case PlaceholderState.NetworkFailure: replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) From d3cb910cf82cb208bf3ae6c3059e8b851107770e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 02:36:50 +0900 Subject: [PATCH 283/587] Convert inline math to not so inline to make operation more explicit --- osu.Game/Online/Leaderboards/Leaderboard.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index e20c8b5007..460869fa54 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -312,12 +312,14 @@ namespace osu.Game.Online.Leaderboards { scrollContainer.Add(scoreFlowContainer = newFlow); - int i = 0; + double delay = 0; foreach (var s in scoreFlowContainer.Children) { - using (s.BeginDelayedSequence(i++ * 50)) + using (s.BeginDelayedSequence(delay)) s.Show(); + + delay += 50; } scrollContainer.ScrollToStart(false); From d21464ea61b8efcf53eb867970b8bf4a5013a109 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 02:54:51 +0900 Subject: [PATCH 284/587] Fix assertions to work in both directions --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 1 + osu.Game/Online/Leaderboards/Leaderboard.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 91ec1de3ad..31b7b9fa8d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -414,6 +414,7 @@ namespace osu.Game.Tests.Visual.SongSelect { public void SetRetrievalState(PlaceholderState state) { + Scores = null; PlaceholderState = state; } diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 460869fa54..7b596d8381 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -243,11 +243,10 @@ namespace osu.Game.Online.Leaderboards placeholderState = value; - Debug.Assert(placeholderState != PlaceholderState.Successful || scores?.Any() == true); - switch (placeholderState) { case PlaceholderState.NetworkFailure: + Debug.Assert(scores?.Any() != true); replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) { Action = RefetchScores @@ -255,26 +254,32 @@ namespace osu.Game.Online.Leaderboards break; case PlaceholderState.NoneSelected: + Debug.Assert(scores?.Any() != true); replacePlaceholder(new MessagePlaceholder(@"Please select a beatmap!")); break; case PlaceholderState.Unavailable: + Debug.Assert(scores?.Any() != true); replacePlaceholder(new MessagePlaceholder(@"Leaderboards are not available for this beatmap!")); break; case PlaceholderState.NoScores: + Debug.Assert(scores?.Any() != true); replacePlaceholder(new MessagePlaceholder(@"No records yet!")); break; case PlaceholderState.NotLoggedIn: + Debug.Assert(scores?.Any() != true); replacePlaceholder(new LoginPlaceholder(@"Please sign in to view online leaderboards!")); break; case PlaceholderState.NotSupporter: + Debug.Assert(scores?.Any() != true); replacePlaceholder(new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!")); break; default: + Debug.Assert(scores?.Any() == true); replacePlaceholder(null); break; } From 9b573fbc2bdc044d0702179fa9b94a82a0fe0648 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 02:58:53 +0900 Subject: [PATCH 285/587] Add missing entries to `switch` statement and guard against out of range --- osu.Game/Online/Leaderboards/Leaderboard.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 7b596d8381..4134046320 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -278,10 +278,18 @@ namespace osu.Game.Online.Leaderboards replacePlaceholder(new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!")); break; - default: + case PlaceholderState.Retrieving: + Debug.Assert(scores?.Any() != true); + replacePlaceholder(null); + break; + + case PlaceholderState.Successful: Debug.Assert(scores?.Any() == true); replacePlaceholder(null); break; + + default: + throw new ArgumentOutOfRangeException(); } } } From 06660ff96019000f7b8302d24fdd0cf28176f460 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 03:02:56 +0900 Subject: [PATCH 286/587] Fix null beatmap in test scene --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 31b7b9fa8d..77fe3be933 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestGlobalScoresDisplay() { AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); - AddStep(@"New Scores", () => leaderboard.Scores = generateSampleScores(null)); + AddStep(@"New Scores", () => leaderboard.Scores = generateSampleScores(new BeatmapInfo())); } [Test] From dad9cc931524f6627c507d5831c9cbd3a79ba6fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 03:06:29 +0900 Subject: [PATCH 287/587] Ensure `Reset`/`Scores_Set` run inline where possible --- osu.Game/Online/Leaderboards/Leaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 4134046320..0ad9f46130 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -66,7 +66,7 @@ namespace osu.Game.Online.Leaderboards protected set { scores = value; - Scheduler.AddOnce(updateScoresDrawables); + Scheduler.Add(updateScoresDrawables, false); } } From b434e29a7c4f4883cb55498678933dc44b4ec185 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 03:10:01 +0900 Subject: [PATCH 288/587] Move loading hide operation inside early return to ensure not hidden too early It should only be hidden after the async load completes. --- osu.Game/Online/Leaderboards/Leaderboard.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 0ad9f46130..8cf20ea8d1 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -303,10 +303,9 @@ namespace osu.Game.Online.Leaderboards .Expire(); scoreFlowContainer = null; - loading.Hide(); - if (scores?.Any() != true) { + loading.Hide(); PlaceholderState = PlaceholderState.NoScores; return; } From 4f4f60248faad019bc3c0501e42de854fea5dbdc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 11 Aug 2021 12:16:25 +0300 Subject: [PATCH 289/587] Add failing test case --- .../TestSceneMultiSpectatorScreen.cs | 51 +++++++++++++++++-- .../Spectate/MultiSpectatorScreen.cs | 5 +- .../Multiplayer/Spectate/PlayerArea.cs | 13 ++++- osu.Game/Screens/Play/Player.cs | 7 +++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index b84f7760e4..56cb6036c7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -347,19 +347,44 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert($"{PLAYER_1_ID} score quit still set", () => getLeaderboardScore(PLAYER_1_ID).HasQuit.Value); } - private void loadSpectateScreen(bool waitForPlayerLoad = true) + /// + /// Tests spectating with a gameplay start time set to a negative value. + /// Simulating beatmaps with high or negative time storyboard elements. + /// + [Test] + public void TestNegativeGameplayStartTime() { - AddStep("load screen", () => + start(PLAYER_1_ID); + + loadSpectateScreen(false, -500); + + // to ensure negative gameplay start time does not affect spectator, send frames exactly after StartGameplay(). + // (similar to real spectating sessions in which the first frames get sent between StartGameplay() and player load complete) + AddStep("send frames at gameplay start", () => getInstance(PLAYER_1_ID).OnGameplayStarted += () => SpectatorClient.SendFrames(PLAYER_1_ID, 100)); + + AddUntilStep("wait for player load", () => spectatorScreen.AllPlayersLoaded); + + AddWaitStep("wait for progression", 3); + + assertNotCatchingUp(PLAYER_1_ID); + assertRunning(PLAYER_1_ID); + } + + private void loadSpectateScreen(bool waitForPlayerLoad = true, double? gameplayStartTime = null) + { + AddStep(!gameplayStartTime.HasValue ? "load screen" : $"load screen (start = {gameplayStartTime}ms)", () => { Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap); Ruleset.Value = importedBeatmap.Ruleset; - LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUsers.ToArray())); + LoadScreen(spectatorScreen = new TestMultiSpectatorScreen(playingUsers.ToArray(), gameplayStartTime)); }); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); } + private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId); + private void start(int[] userIds, int? beatmapId = null) { AddStep("start play", () => @@ -419,6 +444,12 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertMuted(int userId, bool muted) => AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted); + private void assertRunning(int userId) + => AddAssert($"{userId} clock running", () => getInstance(userId).GameplayClock.IsRunning); + + private void assertNotCatchingUp(int userId) + => AddAssert($"{userId} in sync", () => !getInstance(userId).GameplayClock.IsCatchingUp); + private void waitForCatchup(int userId) => AddUntilStep($"{userId} not catching up", () => !getInstance(userId).GameplayClock.IsCatchingUp); @@ -429,5 +460,19 @@ namespace osu.Game.Tests.Visual.Multiplayer private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.Id == userId); private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray(); + + private class TestMultiSpectatorScreen : MultiSpectatorScreen + { + private readonly double? gameplayStartTime; + + public TestMultiSpectatorScreen(MultiplayerRoomUser[] users, double? gameplayStartTime = null) + : base(users) + { + this.gameplayStartTime = gameplayStartTime; + } + + protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) + => new MasterGameplayClockContainer(beatmap, gameplayStartTime ?? 0, gameplayStartTime.HasValue); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 7350408eba..4646f42d63 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; @@ -68,7 +69,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Container leaderboardContainer; Container scoreDisplayContainer; - masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0); + masterClockContainer = CreateMasterGameplayClockContainer(Beatmap.Value); InternalChildren = new[] { @@ -235,5 +236,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return base.OnBackButton(); } + + protected virtual MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) => new MasterGameplayClockContainer(beatmap, 0); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 48f153ecbe..f5a6777a62 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -24,6 +24,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public class PlayerArea : CompositeDrawable { + /// + /// Raised after is called on . + /// + public event Action OnGameplayStarted; + /// /// Whether a is loaded in the area. /// @@ -93,7 +98,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } }; - stack.Push(new MultiSpectatorPlayerLoader(Score, () => new MultiSpectatorPlayer(Score, GameplayClock))); + stack.Push(new MultiSpectatorPlayerLoader(Score, () => + { + var player = new MultiSpectatorPlayer(Score, GameplayClock); + player.OnGameplayStarted += OnGameplayStarted; + return player; + })); + loadingLayer.Hide(); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index cfca2d0a3d..0312789b12 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -45,6 +45,11 @@ namespace osu.Game.Screens.Play /// public const double RESULTS_DISPLAY_DELAY = 1000.0; + /// + /// Raised after is called. + /// + public event Action OnGameplayStarted; + public override bool AllowBackButton => false; // handled by HoldForMenuButton protected override UserActivity InitialActivity => new UserActivity.InSoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); @@ -958,7 +963,9 @@ namespace osu.Game.Screens.Play updateGameplayState(); GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint); + StartGameplay(); + OnGameplayStarted?.Invoke(); } /// From 3ec193d47e9655de747d6de21b042e36ed5f8211 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jan 2022 20:17:57 +0300 Subject: [PATCH 290/587] Fix spectator clock container incorrectly starting catch-up clock --- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs | 2 ++ osu.Game/Screens/Play/GameplayClockContainer.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index ececa1e497..615bd41f3f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -55,6 +55,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public SpectatorGameplayClockContainer([NotNull] IClock sourceClock) : base(sourceClock) { + // the container should initially be in a stopped state until the catch-up clock is started by the sync manager. + Stop(); } protected override void Update() diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 0c9b827a41..0fd524f976 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Play /// /// Stops gameplay. /// - public virtual void Stop() => IsPaused.Value = true; + public void Stop() => IsPaused.Value = true; /// /// Resets this and the source to an initial state ready for gameplay. From c401629dd84e8b91a9389fc85c16cc84f55aa013 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 03:37:57 +0900 Subject: [PATCH 291/587] Also refactor `placeholder` logic to make more sense --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 27 +-- osu.Game/Online/Leaderboards/Leaderboard.cs | 155 +++++++++--------- ...olderState.cs => LeaderboardErrorState.cs} | 5 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 15 +- 4 files changed, 86 insertions(+), 116 deletions(-) rename osu.Game/Online/Leaderboards/{PlaceholderState.cs => LeaderboardErrorState.cs} (82%) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 77fe3be933..969bafa98b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -114,12 +114,12 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestPlaceholderStates() { - AddStep(@"Empty Scores", () => leaderboard.SetRetrievalState(PlaceholderState.NoScores)); - AddStep(@"Network failure", () => leaderboard.SetRetrievalState(PlaceholderState.NetworkFailure)); - AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter)); - AddStep(@"Not logged in", () => leaderboard.SetRetrievalState(PlaceholderState.NotLoggedIn)); - AddStep(@"Unavailable", () => leaderboard.SetRetrievalState(PlaceholderState.Unavailable)); - AddStep(@"None selected", () => leaderboard.SetRetrievalState(PlaceholderState.NoneSelected)); + AddStep(@"Empty Scores", () => leaderboard.SetErrorState(LeaderboardErrorState.NoScores)); + AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardErrorState.NetworkFailure)); + AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardErrorState.NotSupporter)); + AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardErrorState.NotLoggedIn)); + AddStep(@"Unavailable", () => leaderboard.SetErrorState(LeaderboardErrorState.Unavailable)); + AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardErrorState.NoneSelected)); } private void showPersonalBestWithNullPosition() @@ -401,22 +401,9 @@ namespace osu.Game.Tests.Visual.SongSelect }; } - private void showBeatmapWithStatus(BeatmapOnlineStatus status) - { - leaderboard.BeatmapInfo = new BeatmapInfo - { - OnlineID = 1113057, - Status = status, - }; - } - private class FailableLeaderboard : BeatmapLeaderboard { - public void SetRetrievalState(PlaceholderState state) - { - Scores = null; - PlaceholderState = state; - } + public new void SetErrorState(LeaderboardErrorState errorState) => base.SetErrorState(errorState); public new ICollection Scores { diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 8cf20ea8d1..2e552f9774 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -53,6 +53,8 @@ namespace osu.Game.Online.Leaderboards private APIRequest fetchScoresRequest; + private LeaderboardErrorState errorState; + [Resolved(CanBeNull = true)] private IAPIProvider api { get; set; } @@ -169,14 +171,37 @@ namespace osu.Game.Online.Leaderboards RefetchScores(); } + /// + /// Perform a full refetch of scores using current criteria. + /// public void RefetchScores() => Scheduler.AddOnce(refetchScores); + /// + /// Reset the leaderboard into an empty state. + /// protected virtual void Reset() { cancelPendingWork(); Scores = null; } + /// + /// Call when a retrieval or display failure happened to show a relevant message to the user. + /// + /// The state to display. + protected void SetErrorState(LeaderboardErrorState errorState) + { + switch (errorState) + { + case LeaderboardErrorState.NoError: + throw new InvalidOperationException($"State {errorState} cannot be set by a leaderboard implementation."); + } + + Debug.Assert(scores?.Any() != true); + + setErrorState(errorState); + } + /// /// Performs a fetch/refresh of scores to be displayed. /// @@ -195,7 +220,7 @@ namespace osu.Game.Online.Leaderboards Reset(); - PlaceholderState = PlaceholderState.Retrieving; + setErrorState(LeaderboardErrorState.NoError); loading.Show(); currentFetchCancellationSource = new CancellationTokenSource(); @@ -210,7 +235,7 @@ namespace osu.Game.Online.Leaderboards if (e is OperationCanceledException || currentFetchCancellationSource.IsCancellationRequested) return; - PlaceholderState = PlaceholderState.NetworkFailure; + SetErrorState(LeaderboardErrorState.NetworkFailure); }); api?.Queue(fetchScoresRequest); @@ -223,77 +248,6 @@ namespace osu.Game.Online.Leaderboards fetchScoresRequest?.Cancel(); } - #region Placeholder handling - - private Placeholder currentPlaceholder; - - private PlaceholderState placeholderState; - - /// - /// Update the placeholder visibility. - /// Setting this to anything other than PlaceholderState.Successful will cancel all existing retrieval requests and hide scores. - /// - protected PlaceholderState PlaceholderState - { - get => placeholderState; - set - { - if (value == placeholderState) - return; - - placeholderState = value; - - switch (placeholderState) - { - case PlaceholderState.NetworkFailure: - Debug.Assert(scores?.Any() != true); - replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) - { - Action = RefetchScores - }); - break; - - case PlaceholderState.NoneSelected: - Debug.Assert(scores?.Any() != true); - replacePlaceholder(new MessagePlaceholder(@"Please select a beatmap!")); - break; - - case PlaceholderState.Unavailable: - Debug.Assert(scores?.Any() != true); - replacePlaceholder(new MessagePlaceholder(@"Leaderboards are not available for this beatmap!")); - break; - - case PlaceholderState.NoScores: - Debug.Assert(scores?.Any() != true); - replacePlaceholder(new MessagePlaceholder(@"No records yet!")); - break; - - case PlaceholderState.NotLoggedIn: - Debug.Assert(scores?.Any() != true); - replacePlaceholder(new LoginPlaceholder(@"Please sign in to view online leaderboards!")); - break; - - case PlaceholderState.NotSupporter: - Debug.Assert(scores?.Any() != true); - replacePlaceholder(new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!")); - break; - - case PlaceholderState.Retrieving: - Debug.Assert(scores?.Any() != true); - replacePlaceholder(null); - break; - - case PlaceholderState.Successful: - Debug.Assert(scores?.Any() == true); - replacePlaceholder(null); - break; - - default: - throw new ArgumentOutOfRangeException(); - } - } - } - private void updateScoresDrawables() { currentScoresAsyncLoadCancellationSource?.Cancel(); @@ -305,13 +259,14 @@ namespace osu.Game.Online.Leaderboards if (scores?.Any() != true) { + SetErrorState(LeaderboardErrorState.NoScores); loading.Hide(); - PlaceholderState = PlaceholderState.NoScores; return; } // ensure placeholder is hidden when displaying scores - PlaceholderState = PlaceholderState.Successful; + setErrorState(LeaderboardErrorState.NoError); + loading.Show(); LoadComponentAsync(new FillFlowContainer { @@ -339,25 +294,61 @@ namespace osu.Game.Online.Leaderboards }, (currentScoresAsyncLoadCancellationSource = new CancellationTokenSource()).Token); } - private void replacePlaceholder(Placeholder placeholder) + #region Placeholder handling + + private Placeholder placeholder; + + private void setErrorState(LeaderboardErrorState errorState) { - if (placeholder != null && placeholder.Equals(currentPlaceholder)) + if (errorState == this.errorState) return; - currentPlaceholder?.FadeOut(150, Easing.OutQuint).Expire(); + this.errorState = errorState; + + placeholder?.FadeOut(150, Easing.OutQuint).Expire(); + + placeholder = getPlaceholderFor(errorState); if (placeholder == null) - { - currentPlaceholder = null; return; - } placeholderContainer.Child = placeholder; placeholder.ScaleTo(0.8f).Then().ScaleTo(1, fade_duration * 3, Easing.OutQuint); placeholder.FadeInFromZero(fade_duration, Easing.OutQuint); + } - currentPlaceholder = placeholder; + private Placeholder getPlaceholderFor(LeaderboardErrorState errorState) + { + switch (errorState) + { + case LeaderboardErrorState.NetworkFailure: + return new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) + { + Action = RefetchScores + }; + + case LeaderboardErrorState.NoneSelected: + return new MessagePlaceholder(@"Please select a beatmap!"); + + case LeaderboardErrorState.Unavailable: + return new MessagePlaceholder(@"Leaderboards are not available for this beatmap!"); + + case LeaderboardErrorState.NoScores: + return new MessagePlaceholder(@"No records yet!"); + + case LeaderboardErrorState.NotLoggedIn: + return new LoginPlaceholder(@"Please sign in to view online leaderboards!"); + + case LeaderboardErrorState.NotSupporter: + return new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!"); + + case LeaderboardErrorState.NoError: + return null; + + default: + throw new ArgumentOutOfRangeException(); + } } #endregion diff --git a/osu.Game/Online/Leaderboards/PlaceholderState.cs b/osu.Game/Online/Leaderboards/LeaderboardErrorState.cs similarity index 82% rename from osu.Game/Online/Leaderboards/PlaceholderState.cs rename to osu.Game/Online/Leaderboards/LeaderboardErrorState.cs index 297241fa73..17f47bb557 100644 --- a/osu.Game/Online/Leaderboards/PlaceholderState.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardErrorState.cs @@ -3,10 +3,9 @@ namespace osu.Game.Online.Leaderboards { - public enum PlaceholderState + public enum LeaderboardErrorState { - Successful, - Retrieving, + NoError, NetworkFailure, Unavailable, NoneSelected, diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 31868624bb..b0ba830076 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -33,19 +33,12 @@ namespace osu.Game.Screens.Select.Leaderboards set { if (beatmapInfo == null && value == null) - { - // always null scores to ensure a correct initial display. - // see weird `scoresLoadedOnce` logic in base implementation. - Scores = null; return; - } if (beatmapInfo?.Equals(value) == true) return; beatmapInfo = value; - Scores = null; - RefetchScores(); } } @@ -114,7 +107,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (fetchBeatmapInfo == null) { - PlaceholderState = PlaceholderState.NoneSelected; + SetErrorState(LeaderboardErrorState.NoneSelected); return null; } @@ -126,19 +119,19 @@ namespace osu.Game.Screens.Select.Leaderboards if (api?.IsLoggedIn != true) { - PlaceholderState = PlaceholderState.NotLoggedIn; + SetErrorState(LeaderboardErrorState.NotLoggedIn); return null; } if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) { - PlaceholderState = PlaceholderState.Unavailable; + SetErrorState(LeaderboardErrorState.Unavailable); return null; } if (!api.LocalUser.Value.IsSupporter && (Scope != BeatmapLeaderboardScope.Global || filterMods)) { - PlaceholderState = PlaceholderState.NotSupporter; + SetErrorState(LeaderboardErrorState.NotSupporter); return null; } From acc1199addc044c2673516c1738586e16a058a87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 16:16:00 +0900 Subject: [PATCH 292/587] Consolidate flows of `Set` operations, either result or error --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 17 ++--- osu.Game/Online/Leaderboards/Leaderboard.cs | 63 ++++++++----------- .../Match/Components/MatchLeaderboard.cs | 5 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 11 +--- 4 files changed, 37 insertions(+), 59 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 969bafa98b..e9a518b2fd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestGlobalScoresDisplay() { AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); - AddStep(@"New Scores", () => leaderboard.Scores = generateSampleScores(new BeatmapInfo())); + AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()))); } [Test] @@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void showPersonalBestWithNullPosition() { - leaderboard.TopScore = new ScoreInfo + leaderboard.SetScores(leaderboard.Scores, new ScoreInfo { Rank = ScoreRank.XH, Accuracy = 1, @@ -142,12 +142,12 @@ namespace osu.Game.Tests.Visual.SongSelect FlagName = @"ES", }, }, - }; + }); } private void showPersonalBest() { - leaderboard.TopScore = new ScoreInfo + leaderboard.SetScores(leaderboard.Scores, new ScoreInfo { Position = 999, Rank = ScoreRank.XH, @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.SongSelect FlagName = @"ES", }, }, - }; + }); } private void loadMoreScores(Func beatmapInfo) @@ -404,12 +404,7 @@ namespace osu.Game.Tests.Visual.SongSelect private class FailableLeaderboard : BeatmapLeaderboard { public new void SetErrorState(LeaderboardErrorState errorState) => base.SetErrorState(errorState); - - public new ICollection Scores - { - get => base.Scores; - set => base.Scores = value; - } + public new void SetScores(IEnumerable scores, ScoreInfo userScore = default) => base.SetScores(scores, userScore); } } } diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 2e552f9774..9c46739443 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -33,6 +33,11 @@ namespace osu.Game.Online.Leaderboards /// The score model class. public abstract class Leaderboard : CompositeDrawable { + /// + /// The currently displayed scores. + /// + public IEnumerable Scores => scores; + /// /// Whether the current scope should refetch in response to changes in API connectivity state. /// @@ -42,7 +47,7 @@ namespace osu.Game.Online.Leaderboards private readonly OsuScrollContainer scrollContainer; private readonly Container placeholderContainer; - private readonly UserTopScoreContainer topScoreContainer; + private readonly UserTopScoreContainer userScoreContainer; private FillFlowContainer scoreFlowContainer; @@ -62,30 +67,6 @@ namespace osu.Game.Online.Leaderboards private ICollection scores; - public ICollection Scores - { - get => scores; - protected set - { - scores = value; - Scheduler.Add(updateScoresDrawables, false); - } - } - - public TScoreInfo TopScore - { - get => topScoreContainer.Score.Value; - set - { - topScoreContainer.Score.Value = value; - - if (value == null) - topScoreContainer.Hide(); - else - topScoreContainer.Show(); - } - } - private TScope scope; public TScope Scope @@ -133,7 +114,7 @@ namespace osu.Game.Online.Leaderboards { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, - Child = topScoreContainer = new UserTopScoreContainer(CreateDrawableTopScore) + Child = userScoreContainer = new UserTopScoreContainer(CreateDrawableTopScore) }, }, }, @@ -176,15 +157,6 @@ namespace osu.Game.Online.Leaderboards /// public void RefetchScores() => Scheduler.AddOnce(refetchScores); - /// - /// Reset the leaderboard into an empty state. - /// - protected virtual void Reset() - { - cancelPendingWork(); - Scores = null; - } - /// /// Call when a retrieval or display failure happened to show a relevant message to the user. /// @@ -202,6 +174,24 @@ namespace osu.Game.Online.Leaderboards setErrorState(errorState); } + /// + /// Call when score retrieval is ready to be displayed. + /// + /// The scores to display. + /// The user top score, if any. + protected void SetScores(IEnumerable scores, TScoreInfo userScore = default) + { + this.scores = scores?.ToList(); + userScoreContainer.Score.Value = userScore; + + if (userScore == null) + userScoreContainer.Hide(); + else + userScoreContainer.Show(); + + Scheduler.Add(updateScoresDrawables, false); + } + /// /// Performs a fetch/refresh of scores to be displayed. /// @@ -218,7 +208,8 @@ namespace osu.Game.Online.Leaderboards { Debug.Assert(ThreadSafety.IsUpdateThread); - Reset(); + cancelPendingWork(); + SetScores(null); setErrorState(LeaderboardErrorState.NoError); loading.Show(); diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index 1945899a11..ea7de917e2 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components if (id.NewValue == null) return; - Scores = null; + SetScores(null); RefetchScores(); }, true); } @@ -43,8 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components if (cancellationToken.IsCancellationRequested) return; - Scores = r.Leaderboard; - TopScore = r.UserScore; + SetScores(r.Leaderboard, r.UserScore); }; return req; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index b0ba830076..3521a2ef78 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -93,12 +93,6 @@ namespace osu.Game.Screens.Select.Leaderboards }; } - protected override void Reset() - { - base.Reset(); - TopScore = null; - } - protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; protected override APIRequest FetchScores(CancellationToken cancellationToken) @@ -153,8 +147,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (cancellationToken.IsCancellationRequested) return; - Scores = task.GetResultSafely(); - TopScore = r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo); + SetScores(task.GetResultSafely(), r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo)); }), TaskContinuationOptions.OnlyOnRanToCompletion); }; @@ -210,7 +203,7 @@ namespace osu.Game.Screens.Select.Leaderboards scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) .ContinueWith(ordered => { - Scores = ordered.GetResultSafely(); + SetScores(ordered.GetResultSafely()); }, TaskContinuationOptions.OnlyOnRanToCompletion); } } From 04dbb5d3c6cc9c8285fa5d95ac23621761e4a57c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 16:16:33 +0900 Subject: [PATCH 293/587] Disallow setting "NoScores" externally as it is handled internally --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 3 ++- osu.Game/Online/Leaderboards/Leaderboard.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index e9a518b2fd..f91339e060 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -114,7 +114,8 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestPlaceholderStates() { - AddStep(@"Empty Scores", () => leaderboard.SetErrorState(LeaderboardErrorState.NoScores)); + AddStep("ensure no scores displayed", () => leaderboard.SetScores(null)); + AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardErrorState.NetworkFailure)); AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardErrorState.NotSupporter)); AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardErrorState.NotLoggedIn)); diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 9c46739443..9fb0fe0e3b 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -165,6 +165,7 @@ namespace osu.Game.Online.Leaderboards { switch (errorState) { + case LeaderboardErrorState.NoScores: case LeaderboardErrorState.NoError: throw new InvalidOperationException($"State {errorState} cannot be set by a leaderboard implementation."); } @@ -250,7 +251,7 @@ namespace osu.Game.Online.Leaderboards if (scores?.Any() != true) { - SetErrorState(LeaderboardErrorState.NoScores); + setErrorState(LeaderboardErrorState.NoScores); loading.Hide(); return; } From 1dbcb5ab63a7d5656c40f370213148011d3c8fbb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 18:51:15 +0900 Subject: [PATCH 294/587] Add test coverage of intro fail scenario --- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index bfea97410a..40b072c171 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -5,6 +5,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; +using osu.Game.Overlays; using osu.Game.Screens; using osu.Game.Screens.Menu; using osuTK; @@ -22,6 +24,11 @@ namespace osu.Game.Tests.Visual.Menus private IntroScreen intro; + [Cached] + private NotificationOverlay notifications; + + private ScheduledDelegate trackResetDelegate; + protected IntroTestScene() { Children = new Drawable[] @@ -38,6 +45,11 @@ namespace osu.Game.Tests.Visual.Menus RelativePositionAxes = Axes.Both, Depth = float.MinValue, Position = new Vector2(0.5f), + }, + notifications = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, } }; } @@ -63,6 +75,39 @@ namespace osu.Game.Tests.Visual.Menus AddUntilStep("wait for menu", () => intro.DidLoadMenu); } + [Test] + public virtual void TestPlayIntroWithFailingAudioDevice() + { + AddStep("hide notifications", () => notifications.Hide()); + AddStep("restart sequence", () => + { + logo.FinishTransforms(); + logo.IsTracking = false; + + IntroStack?.Expire(); + + Add(IntroStack = new OsuScreenStack + { + RelativeSizeAxes = Axes.Both, + }); + + IntroStack.Push(intro = CreateScreen()); + }); + + AddStep("trigger failure", () => + { + trackResetDelegate = Scheduler.AddDelayed(() => + { + intro.Beatmap.Value.Track.Seek(0); + }, 0, true); + }); + + AddUntilStep("wait for menu", () => intro.DidLoadMenu); + AddUntilStep("wait for notification", () => notifications.UnreadCount.Value == 1); + + AddStep("uninstall delegate", () => trackResetDelegate?.Cancel()); + } + protected abstract IntroScreen CreateScreen(); } } From 52f1c2bfdbc349259bc817ac3e0a4925f9c78a8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 18:37:00 +0900 Subject: [PATCH 295/587] Add failsafe to `IntroScreen` to stop users with incorrect audio configuration getting stuck The most common case of this seems to be linux users with incorrect or unsupported audio driver configurations. It continues to be brought up in discussions as people are unsure of why their game freezes on startup, and unable to easily recover. --- osu.Game/Screens/Menu/IntroScreen.cs | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index fceb083916..98c4b15f7f 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -18,6 +18,7 @@ using osu.Game.Configuration; using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; using osu.Game.Screens.Backgrounds; using osuTK; using osuTK.Graphics; @@ -147,6 +148,36 @@ namespace osu.Game.Screens.Menu } } + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + ensureEventuallyArrivingAtMenu(); + } + + [Resolved] + private NotificationOverlay notifications { get; set; } + + private void ensureEventuallyArrivingAtMenu() + { + // This intends to handle the case where an intro may get stuck. + // Historically, this could happen if the host system's audio device is in a state it can't + // play audio, causing a clock to never elapse time and the intro to never end. + // + // This safety measure gives the user a chance to fix the problem from the settings menu. + Scheduler.AddDelayed(() => + { + if (DidLoadMenu) + return; + + PrepareMenuLoad(); + LoadMenu(); + notifications.Post(new SimpleErrorNotification + { + Text = "osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting." + }); + }, 5000); + } + public override void OnResuming(IScreen last) { this.FadeIn(300); @@ -241,6 +272,9 @@ namespace osu.Game.Screens.Menu protected void PrepareMenuLoad() { + if (nextScreen != null) + return; + nextScreen = createNextScreen?.Invoke(); if (nextScreen != null) @@ -249,6 +283,9 @@ namespace osu.Game.Screens.Menu protected void LoadMenu() { + if (DidLoadMenu) + return; + beatmap.Return(); DidLoadMenu = true; From 6a21d583259b43c0d7865371f34c73d5cba04e04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 18:59:18 +0900 Subject: [PATCH 296/587] Avoid test failures on non-triangle intro tests --- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 6 +++++- osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs | 1 + osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs | 1 + osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index 40b072c171..82accceb23 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -20,6 +20,8 @@ namespace osu.Game.Tests.Visual.Menus [Cached] private OsuLogo logo; + protected abstract bool IntroReliesOnTrack { get; } + protected OsuScreenStack IntroStack; private IntroScreen intro; @@ -103,7 +105,9 @@ namespace osu.Game.Tests.Visual.Menus }); AddUntilStep("wait for menu", () => intro.DidLoadMenu); - AddUntilStep("wait for notification", () => notifications.UnreadCount.Value == 1); + + if (IntroReliesOnTrack) + AddUntilStep("wait for notification", () => notifications.UnreadCount.Value == 1); AddStep("uninstall delegate", () => trackResetDelegate?.Cancel()); } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs index ffc99185fb..7ad49b5dcd 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs @@ -9,6 +9,7 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneIntroCircles : IntroTestScene { + protected override bool IntroReliesOnTrack => false; protected override IntroScreen CreateScreen() => new IntroCircles(); } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs index 8f01e0321b..abe8936330 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs @@ -9,6 +9,7 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneIntroTriangles : IntroTestScene { + protected override bool IntroReliesOnTrack => true; protected override IntroScreen CreateScreen() => new IntroTriangles(); } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 9081be3dd6..11cea25865 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -10,6 +10,7 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneIntroWelcome : IntroTestScene { + protected override bool IntroReliesOnTrack => false; protected override IntroScreen CreateScreen() => new IntroWelcome(); public override void TestPlayIntro() From 82806d7aebc5a0a2273575f4efdbbd6d1c7c866f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 21:32:39 +0900 Subject: [PATCH 297/587] Ensure the background is eventually displayed when `IntroTriangles` suspends --- osu.Game/Screens/Menu/IntroTriangles.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 10f940e9de..b6b6bf2ad7 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -93,6 +93,9 @@ namespace osu.Game.Screens.Menu { base.OnSuspending(next); + // ensure the background is shown, even if the TriangleIntroSequence failed to do so. + background.ApplyToBackground(b => b.Show()); + // important as there is a clock attached to a track which will likely be disposed before returning to this screen. intro.Expire(); } From 1cec76df74df950db682ecde9de14ceeb9c1de8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 23:18:40 +0900 Subject: [PATCH 298/587] Fix weird reading xmldoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Online/Leaderboards/Leaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 9fb0fe0e3b..e96b12bc5d 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -176,7 +176,7 @@ namespace osu.Game.Online.Leaderboards } /// - /// Call when score retrieval is ready to be displayed. + /// Call when retrieved scores are ready to be displayed. /// /// The scores to display. /// The user top score, if any. From f8939af5e6d165d97ad733d0b39634ef6c5b1723 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jan 2022 01:12:03 +0900 Subject: [PATCH 299/587] Track loading via state as well --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 12 ++-- osu.Game/Online/Leaderboards/Leaderboard.cs | 66 ++++++++++--------- ...boardErrorState.cs => LeaderboardState.cs} | 5 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 8 +-- 4 files changed, 48 insertions(+), 43 deletions(-) rename osu.Game/Online/Leaderboards/{LeaderboardErrorState.cs => LeaderboardState.cs} (82%) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index f91339e060..48230ff9e9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -116,11 +116,11 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("ensure no scores displayed", () => leaderboard.SetScores(null)); - AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardErrorState.NetworkFailure)); - AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardErrorState.NotSupporter)); - AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardErrorState.NotLoggedIn)); - AddStep(@"Unavailable", () => leaderboard.SetErrorState(LeaderboardErrorState.Unavailable)); - AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardErrorState.NoneSelected)); + AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardState.NetworkFailure)); + AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardState.NotSupporter)); + AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardState.NotLoggedIn)); + AddStep(@"Unavailable", () => leaderboard.SetErrorState(LeaderboardState.Unavailable)); + AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected)); } private void showPersonalBestWithNullPosition() @@ -404,7 +404,7 @@ namespace osu.Game.Tests.Visual.SongSelect private class FailableLeaderboard : BeatmapLeaderboard { - public new void SetErrorState(LeaderboardErrorState errorState) => base.SetErrorState(errorState); + public new void SetErrorState(LeaderboardState state) => base.SetErrorState(state); public new void SetScores(IEnumerable scores, ScoreInfo userScore = default) => base.SetScores(scores, userScore); } } diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index e96b12bc5d..7ac05fb0c0 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -58,7 +58,7 @@ namespace osu.Game.Online.Leaderboards private APIRequest fetchScoresRequest; - private LeaderboardErrorState errorState; + private LeaderboardState state; [Resolved(CanBeNull = true)] private IAPIProvider api { get; set; } @@ -160,19 +160,20 @@ namespace osu.Game.Online.Leaderboards /// /// Call when a retrieval or display failure happened to show a relevant message to the user. /// - /// The state to display. - protected void SetErrorState(LeaderboardErrorState errorState) + /// The state to display. + protected void SetErrorState(LeaderboardState state) { - switch (errorState) + switch (state) { - case LeaderboardErrorState.NoScores: - case LeaderboardErrorState.NoError: - throw new InvalidOperationException($"State {errorState} cannot be set by a leaderboard implementation."); + case LeaderboardState.NoScores: + case LeaderboardState.Retrieving: + case LeaderboardState.Success: + throw new InvalidOperationException($"State {state} cannot be set by a leaderboard implementation."); } Debug.Assert(scores?.Any() != true); - setErrorState(errorState); + setState(state); } /// @@ -212,8 +213,7 @@ namespace osu.Game.Online.Leaderboards cancelPendingWork(); SetScores(null); - setErrorState(LeaderboardErrorState.NoError); - loading.Show(); + setState(LeaderboardState.Retrieving); currentFetchCancellationSource = new CancellationTokenSource(); @@ -227,7 +227,7 @@ namespace osu.Game.Online.Leaderboards if (e is OperationCanceledException || currentFetchCancellationSource.IsCancellationRequested) return; - SetErrorState(LeaderboardErrorState.NetworkFailure); + SetErrorState(LeaderboardState.NetworkFailure); }); api?.Queue(fetchScoresRequest); @@ -251,15 +251,10 @@ namespace osu.Game.Online.Leaderboards if (scores?.Any() != true) { - setErrorState(LeaderboardErrorState.NoScores); - loading.Hide(); + setState(LeaderboardState.NoScores); return; } - // ensure placeholder is hidden when displaying scores - setErrorState(LeaderboardErrorState.NoError); - loading.Show(); - LoadComponentAsync(new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -269,6 +264,8 @@ namespace osu.Game.Online.Leaderboards ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)) }, newFlow => { + setState(LeaderboardState.Success); + scrollContainer.Add(scoreFlowContainer = newFlow); double delay = 0; @@ -282,7 +279,6 @@ namespace osu.Game.Online.Leaderboards } scrollContainer.ScrollToStart(false); - loading.Hide(); }, (currentScoresAsyncLoadCancellationSource = new CancellationTokenSource()).Token); } @@ -290,16 +286,21 @@ namespace osu.Game.Online.Leaderboards private Placeholder placeholder; - private void setErrorState(LeaderboardErrorState errorState) + private void setState(LeaderboardState state) { - if (errorState == this.errorState) + if (state == this.state) return; - this.errorState = errorState; + if (state == LeaderboardState.Retrieving) + loading.Show(); + else + loading.Hide(); + + this.state = state; placeholder?.FadeOut(150, Easing.OutQuint).Expire(); - placeholder = getPlaceholderFor(errorState); + placeholder = getPlaceholderFor(state); if (placeholder == null) return; @@ -310,32 +311,35 @@ namespace osu.Game.Online.Leaderboards placeholder.FadeInFromZero(fade_duration, Easing.OutQuint); } - private Placeholder getPlaceholderFor(LeaderboardErrorState errorState) + private Placeholder getPlaceholderFor(LeaderboardState state) { - switch (errorState) + switch (state) { - case LeaderboardErrorState.NetworkFailure: + case LeaderboardState.NetworkFailure: return new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) { Action = RefetchScores }; - case LeaderboardErrorState.NoneSelected: + case LeaderboardState.NoneSelected: return new MessagePlaceholder(@"Please select a beatmap!"); - case LeaderboardErrorState.Unavailable: + case LeaderboardState.Unavailable: return new MessagePlaceholder(@"Leaderboards are not available for this beatmap!"); - case LeaderboardErrorState.NoScores: + case LeaderboardState.NoScores: return new MessagePlaceholder(@"No records yet!"); - case LeaderboardErrorState.NotLoggedIn: + case LeaderboardState.NotLoggedIn: return new LoginPlaceholder(@"Please sign in to view online leaderboards!"); - case LeaderboardErrorState.NotSupporter: + case LeaderboardState.NotSupporter: return new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!"); - case LeaderboardErrorState.NoError: + case LeaderboardState.Retrieving: + return null; + + case LeaderboardState.Success: return null; default: diff --git a/osu.Game/Online/Leaderboards/LeaderboardErrorState.cs b/osu.Game/Online/Leaderboards/LeaderboardState.cs similarity index 82% rename from osu.Game/Online/Leaderboards/LeaderboardErrorState.cs rename to osu.Game/Online/Leaderboards/LeaderboardState.cs index 17f47bb557..75e2c6e6db 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardErrorState.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardState.cs @@ -3,9 +3,10 @@ namespace osu.Game.Online.Leaderboards { - public enum LeaderboardErrorState + public enum LeaderboardState { - NoError, + Success, + Retrieving, NetworkFailure, Unavailable, NoneSelected, diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 3521a2ef78..48eb33cc05 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (fetchBeatmapInfo == null) { - SetErrorState(LeaderboardErrorState.NoneSelected); + SetErrorState(LeaderboardState.NoneSelected); return null; } @@ -113,19 +113,19 @@ namespace osu.Game.Screens.Select.Leaderboards if (api?.IsLoggedIn != true) { - SetErrorState(LeaderboardErrorState.NotLoggedIn); + SetErrorState(LeaderboardState.NotLoggedIn); return null; } if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) { - SetErrorState(LeaderboardErrorState.Unavailable); + SetErrorState(LeaderboardState.Unavailable); return null; } if (!api.LocalUser.Value.IsSupporter && (Scope != BeatmapLeaderboardScope.Global || filterMods)) { - SetErrorState(LeaderboardErrorState.NotSupporter); + SetErrorState(LeaderboardState.NotSupporter); return null; } From b52153e73d354f137b221d5c14cd505d37a8ffdd Mon Sep 17 00:00:00 2001 From: Spooghetti420 Date: Sun, 30 Jan 2022 17:40:15 +0000 Subject: [PATCH 300/587] Remove settings --- .../Mods/TestSceneManiaModHoldOff.cs | 58 +++---------------- .../Mods/ManiaModHoldOff.cs | 28 +-------- 2 files changed, 10 insertions(+), 76 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs index e74f63abb3..a113b7ac80 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; @@ -11,8 +10,6 @@ using System.Collections.Generic; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Mania.Difficulty; -using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests.Mods { @@ -42,24 +39,17 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods Assert.AreEqual(noteValues, new List { 0.125, 0.250, 0.500, 1.000, 2.000 }); } - [TestCase(ManiaModHoldOff.BeatDivisors.Whole)] - [TestCase(ManiaModHoldOff.BeatDivisors.Half)] - [TestCase(ManiaModHoldOff.BeatDivisors.Quarter)] - [TestCase(ManiaModHoldOff.BeatDivisors.Eighth)] - public void TestCorrectObjectCount(ManiaModHoldOff.BeatDivisors minBeatSnap) + [Test] + public void TestCorrectObjectCount() { - /* - This test is to ensure that, given that end notes are enabled, - the mod produces the expected number of objects when the mod is applied. - */ + // Ensure that the mod produces the expected number of objects when applied. - // Mod settings will be set to include the correct beat snap value var rawBeatmap = createRawBeatmap(); - var testBeatmap = createModdedBeatmap(minBeatSnap); + var testBeatmap = createModdedBeatmap(); // Calculate expected number of objects int expectedObjectCount = 0; - double beatSnapValue = 1 / (Math.Pow(2, (int)minBeatSnap)); + double beatSnapValue = ManiaModHoldOff.Threshold; foreach (ManiaHitObject h in rawBeatmap.HitObjects) { @@ -81,44 +71,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods Assert.That(testBeatmap.HitObjects.Count == expectedObjectCount); } - [Test] - public void TestDifficultyIncrease() - { - // A lower minimum beat snap divisor should only make the map harder, never easier - // (as more notes can be spawned) - var beatmaps = new[] - { - createModdedBeatmap(), - createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Half), - createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Quarter), - createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Eighth), - createModdedBeatmap(ManiaModHoldOff.BeatDivisors.Sixteenth) - }; - - double[] mapDifficulties = new double[beatmaps.Length]; - - for (int i = 0; i < mapDifficulties.Length; i++) - { - var workingBeatmap = new TestWorkingBeatmap(beatmaps[i]); - var difficultyCalculator = new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, workingBeatmap); - mapDifficulties[i] = difficultyCalculator.Calculate().StarRating; - - if (i > 0) - { - Assert.LessOrEqual(mapDifficulties[i - 1], mapDifficulties[i]); - Assert.LessOrEqual(beatmaps[i - 1].HitObjects.Count, beatmaps[i].HitObjects.Count); - } - } - } - - private static ManiaBeatmap createModdedBeatmap(ManiaModHoldOff.BeatDivisors minBeatSnap = ManiaModHoldOff.BeatDivisors.Whole) + private static ManiaBeatmap createModdedBeatmap() { var beatmap = createRawBeatmap(); - var holdOffMod = new ManiaModHoldOff - { - MinBeatSnap = { Value = minBeatSnap } - }; - Assert.AreEqual(holdOffMod.MinBeatSnap.Value, minBeatSnap); + var holdOffMod = new ManiaModHoldOff(); foreach (var hitObject in beatmap.HitObjects) hitObject.ApplyDefaults(beatmap.ControlPointInfo, new BeatmapDifficulty()); diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index 637142aed6..5a47e1ba89 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -6,11 +6,8 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; using osu.Framework.Graphics.Sprites; -using System; using System.Collections.Generic; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Framework.Bindables; -using osu.Game.Configuration; namespace osu.Game.Rulesets.Mania.Mods { @@ -27,23 +24,13 @@ namespace osu.Game.Rulesets.Mania.Mods public override IconUsage? Icon => FontAwesome.Solid.DotCircle; public override ModType Type => ModType.Conversion; - - [SettingSource("Add end notes", "Also add a note at the end of a hold note")] - public BindableBool AddEndNotes { get; } = new BindableBool - { - Default = true, - Value = true - }; - - [SettingSource("Minimum end note beat snap", "Don't add end notes for hold notes shorter than this beat division")] - public Bindable MinBeatSnap { get; } = new Bindable(defaultValue: BeatDivisors.Half); + public const double Threshold = 1/2; public void ApplyToBeatmap(IBeatmap beatmap) { var maniaBeatmap = (ManiaBeatmap)beatmap; var newObjects = new List(); - double beatSnap = 1 / (Math.Pow(2, (double)MinBeatSnap.Value)); foreach (var h in beatmap.HitObjects.OfType()) { @@ -55,10 +42,10 @@ namespace osu.Game.Rulesets.Mania.Mods Samples = h.GetNodeSamples(0) }); - // Don't add an end note if the duration is shorter than some threshold, or end notes are disabled + // Don't add an end note if the duration is shorter than the threshold double noteValue = GetNoteDurationInBeatLength(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc. - if (AddEndNotes.Value && noteValue >= beatSnap) + if (noteValue >= Threshold) { newObjects.Add(new Note { @@ -77,14 +64,5 @@ namespace osu.Game.Rulesets.Mania.Mods double bpmAtNoteTime = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BPM; return (60 * holdNote.Duration) / (1000 * bpmAtNoteTime); } - - public enum BeatDivisors - { - Whole, - Half, - Quarter, - Eighth, - Sixteenth - } } } From 2b999f9780f4f9e96c92fbe42e4a71a86605b0ec Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jan 2022 23:39:45 +0300 Subject: [PATCH 301/587] Add failing test case --- .../TestSceneMasterGameplayClockContainer.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs index 935bc07733..172531605a 100644 --- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs @@ -2,7 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; using osu.Game.Tests.Visual; @@ -12,6 +18,14 @@ namespace osu.Game.Tests.Gameplay [HeadlessTest] public class TestSceneMasterGameplayClockContainer : OsuTestScene { + private OsuConfigManager localConfig; + + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage)); + } + [Test] public void TestStartThenElapsedTime() { @@ -54,5 +68,43 @@ namespace osu.Game.Tests.Gameplay AddAssert("current time < time at reset", () => gcc.GameplayClock.CurrentTime < timeAtReset); } + + [Test] + public void TestSeekPerformsInGameplayTime( + [Values(1.0, 0.5, 2.0)] double clockRate, + [Values(0.0, 200.0, -200.0)] double userOffset, + [Values(false, true)] bool whileStopped) + { + ClockBackedTestWorkingBeatmap working = null; + GameplayClockContainer gcc = null; + + AddStep("create container", () => + { + working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio); + working.LoadTrack(); + + Add(gcc = new MasterGameplayClockContainer(working, 0)); + + if (whileStopped) + gcc.Stop(); + + gcc.Reset(); + }); + + AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate))); + AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset)); + + AddStep("seek to 2500", () => gcc.Seek(2500)); + AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gcc.CurrentTime, 2500, 10f)); + + AddStep("seek to 10000", () => gcc.Seek(10000)); + AddAssert("gameplay clock time = 10000", () => Precision.AlmostEquals(gcc.CurrentTime, 10000, 10f)); + } + + protected override void Dispose(bool isDisposing) + { + localConfig?.Dispose(); + base.Dispose(isDisposing); + } } } From 6556a7e3c3533b46a1f6b97989d2d6bf4f908a4a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jan 2022 23:06:31 +0300 Subject: [PATCH 302/587] Handle different gameplay rates when seeking on master clock --- .../Play/MasterGameplayClockContainer.cs | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index aa46522dec..200921680e 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Play Precision = 0.1, }; - private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; + private double totalAppliedOffset => userOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); @@ -52,8 +52,8 @@ namespace osu.Game.Screens.Play private readonly bool startAtGameplayStart; private readonly double firstHitObjectTime; - private FramedOffsetClock userOffsetClock; - private FramedOffsetClock platformOffsetClock; + private HardwareCorrectionOffsetClock userOffsetClock; + private HardwareCorrectionOffsetClock platformOffsetClock; private MasterGameplayClock masterGameplayClock; private Bindable userAudioOffset; private double startOffset; @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Play { // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. // we may want to consider reversing the application of offsets in the future as it may feel more correct. - base.Seek(time - totalOffset); + base.Seek(time - totalAppliedOffset); } /// @@ -214,13 +214,25 @@ namespace osu.Game.Screens.Play private class HardwareCorrectionOffsetClock : FramedOffsetClock { - // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. - // base implementation already adds offset at 1.0 rate, so we only add the difference from that here. - public override double CurrentTime => base.CurrentTime + offsetAdjust; - private readonly BindableDouble pauseRateAdjust; - private double offsetAdjust; + private double offset; + + public new double Offset + { + get => offset; + set + { + if (value == offset) + return; + + offset = value; + + updateOffset(); + } + } + + public double RateAdjustedOffset => base.Offset; public HardwareCorrectionOffsetClock(IClock source, BindableDouble pauseRateAdjust) : base(source) @@ -231,10 +243,17 @@ namespace osu.Game.Screens.Play public override void ProcessFrame() { base.ProcessFrame(); + updateOffset(); + } + private void updateOffset() + { // changing this during the pause transform effect will cause a potentially large offset to be suddenly applied as we approach zero rate. if (pauseRateAdjust.Value == 1) - offsetAdjust = Offset * (Rate - 1); + { + // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. + base.Offset = Offset * Rate; + } } } From bb8dc74e88fb3fcc3ce6958d740199a9249bc11b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jan 2022 12:20:51 +0900 Subject: [PATCH 303/587] Fix constant formatting --- .../Mods/TestSceneManiaModHoldOff.cs | 3 +-- osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs index a113b7ac80..7970d5b594 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs @@ -49,7 +49,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods // Calculate expected number of objects int expectedObjectCount = 0; - double beatSnapValue = ManiaModHoldOff.Threshold; foreach (ManiaHitObject h in rawBeatmap.HitObjects) { @@ -60,7 +59,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { double noteValue = ManiaModHoldOff.GetNoteDurationInBeatLength((HoldNote)h, rawBeatmap); - if (noteValue >= beatSnapValue) + if (noteValue >= ManiaModHoldOff.END_NOTE_ALLOW_THRESHOLD) { // Should generate an end note if it's longer than the minimum note value expectedObjectCount++; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index 5a47e1ba89..42dc7fdc1f 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -24,7 +24,8 @@ namespace osu.Game.Rulesets.Mania.Mods public override IconUsage? Icon => FontAwesome.Solid.DotCircle; public override ModType Type => ModType.Conversion; - public const double Threshold = 1/2; + + public const double END_NOTE_ALLOW_THRESHOLD = 0.5; public void ApplyToBeatmap(IBeatmap beatmap) { @@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Mods // Don't add an end note if the duration is shorter than the threshold double noteValue = GetNoteDurationInBeatLength(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc. - if (noteValue >= Threshold) + if (noteValue >= END_NOTE_ALLOW_THRESHOLD) { newObjects.Add(new Note { From 610eb9f6a45e8f3f762bac4f25a814c7169d8010 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jan 2022 13:45:49 +0900 Subject: [PATCH 304/587] Remove unnecessary container level --- osu.Game/Online/Leaderboards/Leaderboard.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 7ac05fb0c0..5dd3e46b4a 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -110,12 +110,7 @@ namespace osu.Game.Online.Leaderboards }, new Drawable[] { - new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Child = userScoreContainer = new UserTopScoreContainer(CreateDrawableTopScore) - }, + userScoreContainer = new UserTopScoreContainer(CreateDrawableTopScore) }, }, }, From 9c9fda84f3cf54073c7d15b62946251a3a27d328 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jan 2022 13:50:53 +0900 Subject: [PATCH 305/587] Add schedule and cancellation check to score ordering step --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 48eb33cc05..907a2c9bda 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -201,10 +201,13 @@ namespace osu.Game.Screens.Select.Leaderboards scores = scores.Detach(); scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) - .ContinueWith(ordered => + .ContinueWith(ordered => Schedule(() => { + if (cancellationToken.IsCancellationRequested) + return; + SetScores(ordered.GetResultSafely()); - }, TaskContinuationOptions.OnlyOnRanToCompletion); + }), TaskContinuationOptions.OnlyOnRanToCompletion); } } From a06287e76aac73bbf141d07a71fa417c7dbb31d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 12:34:08 +0900 Subject: [PATCH 306/587] Remove `DrawableCarouselItem.Update` updating of height Marginal from a performance aspect, but reads better. --- .../Select/Carousel/DrawableCarouselBeatmap.cs | 7 +++---- .../Select/Carousel/DrawableCarouselItem.cs | 14 ++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 3576b77ae8..43ddfc79b1 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -36,9 +36,9 @@ namespace osu.Game.Screens.Select.Carousel /// /// The height of a carousel beatmap, including vertical spacing. /// - public const float HEIGHT = height + CAROUSEL_BEATMAP_SPACING; + public const float HEIGHT = header_height + CAROUSEL_BEATMAP_SPACING; - private const float height = MAX_HEIGHT * 0.6f; + private const float header_height = MAX_HEIGHT * 0.6f; private readonly BeatmapInfo beatmapInfo; @@ -67,6 +67,7 @@ namespace osu.Game.Screens.Select.Carousel private CancellationTokenSource starDifficultyCancellationSource; public DrawableCarouselBeatmap(CarouselBeatmap panel) + : base(header_height) { beatmapInfo = panel.BeatmapInfo; Item = panel; @@ -75,8 +76,6 @@ namespace osu.Game.Screens.Select.Carousel [BackgroundDependencyLoader(true)] private void load(BeatmapManager manager, SongSelect songSelect) { - Header.Height = height; - if (songSelect != null) { startRequested = b => songSelect.FinaliseSelection(b); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index cde3edad39..8b38bb9a96 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Select.Carousel } } - protected DrawableCarouselItem() + protected DrawableCarouselItem(float headerHeight = MAX_HEIGHT) { RelativeSizeAxes = Axes.X; @@ -73,10 +73,14 @@ namespace osu.Game.Screens.Select.Carousel RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - Header = new CarouselHeader(), + Header = new CarouselHeader + { + Height = headerHeight, + }, Content = new Container { RelativeSizeAxes = Axes.Both, + Y = headerHeight, } } }, @@ -92,12 +96,6 @@ namespace osu.Game.Screens.Select.Carousel UpdateItem(); } - protected override void Update() - { - base.Update(); - Content.Y = Header.Height; - } - protected virtual void UpdateItem() { if (item == null) From c3e3b2019dcf169d74c435bde269ec53bf43a8fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 13:01:41 +0900 Subject: [PATCH 307/587] Reduce overhead of `ApplyState` by tracking previous values Even with pooling applied, there are overheads involved with transforms when quickly cycling through the carousel. The main goal here is to reduce the transforms in cases the reuse is still in the same state. Avoiding firing `FadeIn` and `FadeOut` are the main areas of saving. --- .../Carousel/DrawableCarouselBeatmap.cs | 3 + .../Select/Carousel/DrawableCarouselItem.cs | 55 ++++++++++++++----- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 43ddfc79b1..a3483aa60a 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -71,6 +71,9 @@ namespace osu.Game.Screens.Select.Carousel { beatmapInfo = panel.BeatmapInfo; Item = panel; + + // Difficulty panels should start hidden for a better initial effect. + Hide(); } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 8b38bb9a96..4a23faa650 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -64,8 +64,6 @@ namespace osu.Game.Screens.Select.Carousel { RelativeSizeAxes = Axes.X; - Alpha = 0; - InternalChildren = new Drawable[] { MovementContainer = new Container @@ -119,29 +117,56 @@ namespace osu.Game.Screens.Select.Carousel private void onStateChange(ValueChangedEvent _) => Scheduler.AddOnce(ApplyState); + private CarouselItemState? lastAppliedState; + protected virtual void ApplyState() { - // Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead. - // Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away. - Height = Item.TotalHeight; - Debug.Assert(Item != null); - switch (Item.State.Value) + if (lastAppliedState != Item.State.Value) { - case CarouselItemState.NotSelected: - Deselected(); - break; + lastAppliedState = Item.State.Value; - case CarouselItemState.Selected: - Selected(); - break; + // Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead. + // Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away. + Height = Item.TotalHeight; + + switch (lastAppliedState) + { + case CarouselItemState.NotSelected: + Deselected(); + break; + + case CarouselItemState.Selected: + Selected(); + break; + } } if (!Item.Visible) - this.FadeOut(300, Easing.OutQuint); + Hide(); else - this.FadeIn(250); + Show(); + } + + private bool isVisible = true; + + public override void Show() + { + if (isVisible) + return; + + isVisible = true; + this.FadeIn(250); + } + + public override void Hide() + { + if (!isVisible) + return; + + isVisible = false; + this.FadeOut(300, Easing.OutQuint); } protected virtual void Selected() From 2ee0db0ebf68bc6651171c78e077418e952d71dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 14:07:29 +0900 Subject: [PATCH 308/587] Move fade in function local --- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 618c5cf5ec..4f19e011b2 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -124,9 +124,9 @@ namespace osu.Game.Screens.Select.Carousel background.DelayedLoadComplete += fadeContentIn; mainFlow.DelayedLoadComplete += fadeContentIn; - } - private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint); + static void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint); + } protected override void Deselected() { From 8917ab78f427c1dfb2e5eef47738fafec8fa746a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jan 2022 14:14:50 +0900 Subject: [PATCH 309/587] Reduce unnecessary container nesting and adjust empty state opacity slightly --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 ++-- .../Screens/Select/Carousel/CarouselHeader.cs | 29 ++++++++----------- .../Select/Carousel/DrawableCarouselItem.cs | 2 -- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c27915c383..fbb12a86d3 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -894,10 +894,8 @@ namespace osu.Game.Screens.Select // child items (difficulties) are still visible. item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0); - // We are applying a multiplicative alpha (which is internally done by nesting an - // additional container and setting that container's alpha) such that we can - // layer alpha transformations on top. - item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); + // We are applying alpha to the header here such that we can layer alpha transformations on top. + item.Header.Alpha = Math.Clamp(1.75f - 1.5f * dist, 0, 1); } private enum PendingScrollOperation diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index ed3aea3445..533694b265 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -21,8 +21,6 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselHeader : Container { - public Container BorderContainer; - public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); private readonly HoverLayer hoverLayer; @@ -37,17 +35,14 @@ namespace osu.Game.Screens.Select.Carousel RelativeSizeAxes = Axes.X; Height = DrawableCarouselItem.MAX_HEIGHT; - InternalChild = BorderContainer = new Container + Masking = true; + CornerRadius = corner_radius; + BorderColour = new Color4(221, 255, 255, 255); + + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = corner_radius, - BorderColour = new Color4(221, 255, 255, 255), - Children = new Drawable[] - { - Content, - hoverLayer = new HoverLayer() - } + Content, + hoverLayer = new HoverLayer() }; } @@ -66,21 +61,21 @@ namespace osu.Game.Screens.Select.Carousel case CarouselItemState.NotSelected: hoverLayer.InsetForBorder = false; - BorderContainer.BorderThickness = 0; - BorderContainer.EdgeEffect = new EdgeEffectParameters + BorderThickness = 0; + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Offset = new Vector2(1), Radius = 10, - Colour = Color4.Black.Opacity(100), + Colour = Color4.Black.Opacity(0.5f), }; break; case CarouselItemState.Selected: hoverLayer.InsetForBorder = true; - BorderContainer.BorderThickness = border_thickness; - BorderContainer.EdgeEffect = new EdgeEffectParameters + BorderThickness = border_thickness; + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = new Color4(130, 204, 255, 150), diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 4a23faa650..5e7ca0825a 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -85,8 +85,6 @@ namespace osu.Game.Screens.Select.Carousel }; } - public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha; - protected override void LoadComplete() { base.LoadComplete(); From 6bc6675fa1bff93de775e10b6cf6781af9bdb3f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jan 2022 14:46:16 +0900 Subject: [PATCH 310/587] Adjust fade in times slightly --- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 4f19e011b2..63c004f4bc 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -122,10 +122,8 @@ namespace osu.Game.Screens.Select.Carousel }, }; - background.DelayedLoadComplete += fadeContentIn; - mainFlow.DelayedLoadComplete += fadeContentIn; - - static void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint); + background.DelayedLoadComplete += d => d.FadeInFromZero(750, Easing.OutQuint); + mainFlow.DelayedLoadComplete += d => d.FadeInFromZero(500, Easing.OutQuint); } protected override void Deselected() From 57f793aff0e6f562198b732dea85f9bd8c154dee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jan 2022 15:12:08 +0900 Subject: [PATCH 311/587] Rename dictionary and make `private` for added safety --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 +- osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index d1a25b07ec..917b3c89a8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); // We can't access API because we're an "online" test. - AddAssert("last received frame has time = 1000", () => spectatorClient.LastReceivedFrame.First().Value.Time == 1000); + AddAssert("last received frame has time = 1000", () => spectatorClient.LastReceivedUserFrames.First().Value.Time == 1000); } private OsuFramedReplayInputHandler replayHandler => diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 3f3d0f5b01..1a1d493249 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -28,7 +28,10 @@ namespace osu.Game.Tests.Visual.Spectator public override IBindable IsConnected { get; } = new Bindable(true); - public readonly Dictionary LastReceivedFrame = new Dictionary(); + public IReadOnlyDictionary LastReceivedUserFrames => lastReceivedUserFrames; + + private readonly Dictionary lastReceivedUserFrames = new Dictionary(); + private readonly Dictionary userBeatmapDictionary = new Dictionary(); private readonly Dictionary userNextFrameDictionary = new Dictionary(); @@ -40,7 +43,7 @@ namespace osu.Game.Tests.Visual.Spectator OnNewFrames += (i, bundle) => { if (PlayingUsers.Contains(i)) - LastReceivedFrame[i] = bundle.Frames[^1]; + lastReceivedUserFrames[i] = bundle.Frames[^1]; }; } From 9001c3a396a7f876bc52d772e1567d62161770f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jan 2022 15:17:06 +0900 Subject: [PATCH 312/587] Fix test failures if `DialogOverlay` is not loaded in time As seen at https://github.com/ppy/osu/runs/4999391205?check_suite_focus=true, where `DialogOverlay` hasn't loaded in single file yet. --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 3be7cf7c5c..ed484e03f6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -128,7 +128,9 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("choose clear all scores", () => InputManager.Key(Key.Number4)); + AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null); AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); + AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null); AddAssert("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == true)); @@ -170,7 +172,9 @@ namespace osu.Game.Tests.Visual.Navigation InputManager.Click(MouseButton.Left); }); + AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null); AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); + AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null); AddAssert("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find(score.ID)?.DeletePending == true)); From 25cbe5a0dee2ef50bdf32bdc8a2b09bc9c98446d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jan 2022 15:23:31 +0900 Subject: [PATCH 313/587] Remove acronym shortening of `GameplayClockContainer` --- .../TestSceneMasterGameplayClockContainer.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs index 172531605a..77b402ad3c 100644 --- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs @@ -29,44 +29,44 @@ namespace osu.Game.Tests.Gameplay [Test] public void TestStartThenElapsedTime() { - GameplayClockContainer gcc = null; + GameplayClockContainer gameplayClockContainer = null; AddStep("create container", () => { var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); working.LoadTrack(); - Add(gcc = new MasterGameplayClockContainer(working, 0)); + Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); }); - AddStep("start clock", () => gcc.Start()); - AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0); + AddStep("start clock", () => gameplayClockContainer.Start()); + AddUntilStep("elapsed greater than zero", () => gameplayClockContainer.GameplayClock.ElapsedFrameTime > 0); } [Test] public void TestElapseThenReset() { - GameplayClockContainer gcc = null; + GameplayClockContainer gameplayClockContainer = null; AddStep("create container", () => { var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); working.LoadTrack(); - Add(gcc = new MasterGameplayClockContainer(working, 0)); + Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); }); - AddStep("start clock", () => gcc.Start()); - AddUntilStep("current time greater 2000", () => gcc.GameplayClock.CurrentTime > 2000); + AddStep("start clock", () => gameplayClockContainer.Start()); + AddUntilStep("current time greater 2000", () => gameplayClockContainer.GameplayClock.CurrentTime > 2000); double timeAtReset = 0; AddStep("reset clock", () => { - timeAtReset = gcc.GameplayClock.CurrentTime; - gcc.Reset(); + timeAtReset = gameplayClockContainer.GameplayClock.CurrentTime; + gameplayClockContainer.Reset(); }); - AddAssert("current time < time at reset", () => gcc.GameplayClock.CurrentTime < timeAtReset); + AddAssert("current time < time at reset", () => gameplayClockContainer.GameplayClock.CurrentTime < timeAtReset); } [Test] @@ -76,29 +76,29 @@ namespace osu.Game.Tests.Gameplay [Values(false, true)] bool whileStopped) { ClockBackedTestWorkingBeatmap working = null; - GameplayClockContainer gcc = null; + GameplayClockContainer gameplayClockContainer = null; AddStep("create container", () => { working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio); working.LoadTrack(); - Add(gcc = new MasterGameplayClockContainer(working, 0)); + Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); if (whileStopped) - gcc.Stop(); + gameplayClockContainer.Stop(); - gcc.Reset(); + gameplayClockContainer.Reset(); }); AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate))); AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset)); - AddStep("seek to 2500", () => gcc.Seek(2500)); - AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gcc.CurrentTime, 2500, 10f)); + AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500)); + AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f)); - AddStep("seek to 10000", () => gcc.Seek(10000)); - AddAssert("gameplay clock time = 10000", () => Precision.AlmostEquals(gcc.CurrentTime, 10000, 10f)); + AddStep("seek to 10000", () => gameplayClockContainer.Seek(10000)); + AddAssert("gameplay clock time = 10000", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 10000, 10f)); } protected override void Dispose(bool isDisposing) From cc7fb0e559b096566b2874607f2ff5f57ca8e0e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jan 2022 16:36:56 +0900 Subject: [PATCH 314/587] Add mouse click support and increase area to full column height --- osu.Game.Rulesets.Mania/UI/Column.cs | 72 +++++++++++++++------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index cd5f3d2170..9bcd9ad853 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -28,7 +28,6 @@ namespace osu.Game.Rulesets.Mania.UI { public const float COLUMN_WIDTH = 80; public const float SPECIAL_COLUMN_WIDTH = 70; - public const float TOUCH_AREA_HEIGHT = 100; /// /// The index of this column as part of the whole playfield. @@ -45,37 +44,6 @@ namespace osu.Game.Rulesets.Mania.UI private readonly GameplaySampleTriggerSource sampleTriggerSource; - public class ColumnTouchInputArea : Drawable - { - public ColumnTouchInputArea() - { - RelativeSizeAxes = Axes.X; - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; - Height = TOUCH_AREA_HEIGHT; - } - - private Column column => (Column)Parent; - - protected override void LoadComplete() - { - keyBindingContainer = (ManiaInputManager.RulesetKeyBindingContainer)((ManiaInputManager)GetContainingInputManager()).KeyBindingContainer; - } - - private ManiaInputManager.RulesetKeyBindingContainer keyBindingContainer { get; set; } - - protected override bool OnTouchDown(TouchDownEvent e) - { - keyBindingContainer.TriggerPressed(column.Action.Value); - return false; - } - - protected override void OnTouchUp(TouchUpEvent e) - { - keyBindingContainer.TriggerReleased(column.Action.Value); - } - } - public Column(int index) { Index = index; @@ -172,5 +140,45 @@ namespace osu.Game.Rulesets.Mania.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); + + public class ColumnTouchInputArea : Drawable + { + public ColumnTouchInputArea() + { + RelativeSizeAxes = Axes.Both; + } + + private Column column => (Column)Parent; + + protected override void LoadComplete() + { + keyBindingContainer = (ManiaInputManager.RulesetKeyBindingContainer)((ManiaInputManager)GetContainingInputManager()).KeyBindingContainer; + } + + private ManiaInputManager.RulesetKeyBindingContainer keyBindingContainer { get; set; } + + protected override bool OnMouseDown(MouseDownEvent e) + { + keyBindingContainer.TriggerPressed(column.Action.Value); + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + keyBindingContainer.TriggerReleased(column.Action.Value); + base.OnMouseUp(e); + } + + protected override bool OnTouchDown(TouchDownEvent e) + { + keyBindingContainer.TriggerPressed(column.Action.Value); + return true; + } + + protected override void OnTouchUp(TouchUpEvent e) + { + keyBindingContainer.TriggerReleased(column.Action.Value); + } + } } } From 9005bce0fa574ec9be7661bbeb5029e1ad8797fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jan 2022 16:37:47 +0900 Subject: [PATCH 315/587] Add "counter" keyword for key overlay setting --- osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs index e1b452e322..ba9779d650 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -37,7 +37,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay, - Current = config.GetBindable(OsuSetting.KeyOverlay) + Current = config.GetBindable(OsuSetting.KeyOverlay), + Keywords = new[] { "counter" }, }, }; } From 0e764538e0ea81293ef99994b25e3ab7ceed609e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jan 2022 16:47:20 +0900 Subject: [PATCH 316/587] Retrieve `KeyBindingContainer` via DI rather than traversal lookup --- osu.Game.Rulesets.Mania/ManiaInputManager.cs | 2 ++ osu.Game.Rulesets.Mania/UI/Column.cs | 17 ++++++++++------- osu.Game/Rulesets/UI/RulesetInputManager.cs | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index 186fc4b15d..14ca27a11a 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.ComponentModel; +using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania { + [Cached] // Used for touch input, see ColumnTouchInputArea. public class ManiaInputManager : RulesetInputManager { public ManiaInputManager(RulesetInfo ruleset, int variant) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 9bcd9ad853..a6c64e9b2d 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -143,6 +143,11 @@ namespace osu.Game.Rulesets.Mania.UI public class ColumnTouchInputArea : Drawable { + [Resolved(canBeNull: true)] + private ManiaInputManager maniaInputManager { get; set; } + + private KeyBindingContainer keyBindingContainer; + public ColumnTouchInputArea() { RelativeSizeAxes = Axes.Both; @@ -152,32 +157,30 @@ namespace osu.Game.Rulesets.Mania.UI protected override void LoadComplete() { - keyBindingContainer = (ManiaInputManager.RulesetKeyBindingContainer)((ManiaInputManager)GetContainingInputManager()).KeyBindingContainer; + keyBindingContainer = maniaInputManager?.KeyBindingContainer; } - private ManiaInputManager.RulesetKeyBindingContainer keyBindingContainer { get; set; } - protected override bool OnMouseDown(MouseDownEvent e) { - keyBindingContainer.TriggerPressed(column.Action.Value); + keyBindingContainer?.TriggerPressed(column.Action.Value); return base.OnMouseDown(e); } protected override void OnMouseUp(MouseUpEvent e) { - keyBindingContainer.TriggerReleased(column.Action.Value); + keyBindingContainer?.TriggerReleased(column.Action.Value); base.OnMouseUp(e); } protected override bool OnTouchDown(TouchDownEvent e) { - keyBindingContainer.TriggerPressed(column.Action.Value); + keyBindingContainer?.TriggerPressed(column.Action.Value); return true; } protected override void OnTouchUp(TouchUpEvent e) { - keyBindingContainer.TriggerReleased(column.Action.Value); + keyBindingContainer?.TriggerReleased(column.Action.Value); } } } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 160c0a2606..84e42818be 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.UI public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler where T : struct { + public readonly KeyBindingContainer KeyBindingContainer; + private ReplayRecorder recorder; public ReplayRecorder Recorder @@ -43,8 +45,6 @@ namespace osu.Game.Rulesets.UI protected override InputState CreateInitialState() => new RulesetInputManagerInputState(base.CreateInitialState()); - public readonly KeyBindingContainer KeyBindingContainer; - protected override Container Content => content; private readonly Container content; From a49a9ed0a05d778b5af4c6328071bb0f8926d7b5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 31 Jan 2022 17:19:04 +0900 Subject: [PATCH 317/587] Fix incorrect invoke --- osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index f5a6777a62..4979bd906b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate stack.Push(new MultiSpectatorPlayerLoader(Score, () => { var player = new MultiSpectatorPlayer(Score, GameplayClock); - player.OnGameplayStarted += OnGameplayStarted; + player.OnGameplayStarted += () => OnGameplayStarted?.Invoke(); return player; })); From 4727aeda012f6c5f00f500d0eaf4c05ba8bbc58c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 31 Jan 2022 18:32:17 +0900 Subject: [PATCH 318/587] Give last bundled replay frame the frame header --- .../Visual/Gameplay/TestSceneSpectator.cs | 16 ++++++++++++++++ osu.Game/Online/Spectator/SpectatorClient.cs | 3 +++ osu.Game/Rulesets/Replays/ReplayFrame.cs | 13 +++++++++++++ osu.Game/Screens/Play/SpectatorPlayer.cs | 1 + 4 files changed, 33 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 917b3c89a8..a7b9d45f7a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -218,6 +218,22 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("last received frame has time = 1000", () => spectatorClient.LastReceivedUserFrames.First().Value.Time == 1000); } + [Test] + public void TestFinalFrameInBundleHasHeader() + { + FrameDataBundle lastBundle = null; + + AddStep("bind to client", () => spectatorClient.OnNewFrames += (_, bundle) => lastBundle = bundle); + + start(-1234); + sendFrames(); + finish(); + + AddUntilStep("bundle received", () => lastBundle != null); + AddAssert("first frame does not have header", () => lastBundle.Frames[0].Header == null); + AddAssert("last frame has header", () => lastBundle.Frames[^1].Header != null); + } + private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index fddb94fad7..67aa75727d 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -129,6 +129,9 @@ namespace osu.Game.Online.Spectator Task ISpectatorClient.UserSentFrames(int userId, FrameDataBundle data) { + if (data.Frames.Count > 0) + data.Frames[^1].Header = data.Header; + Schedule(() => OnNewFrames?.Invoke(userId, data)); return Task.CompletedTask; diff --git a/osu.Game/Rulesets/Replays/ReplayFrame.cs b/osu.Game/Rulesets/Replays/ReplayFrame.cs index 7de53211a2..2b67b60d8f 100644 --- a/osu.Game/Rulesets/Replays/ReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/ReplayFrame.cs @@ -1,16 +1,29 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using MessagePack; +using osu.Game.Online.Spectator; namespace osu.Game.Rulesets.Replays { [MessagePackObject] public class ReplayFrame { + /// + /// The time at which this takes place. + /// [Key(0)] public double Time; + /// + /// A containing the state of a play after this takes place. + /// May be omitted where exact per-frame accuracy is not required. + /// + [IgnoreMember] + public FrameHeader? Header; + public ReplayFrame() { } diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index d42643c416..c415041081 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -72,6 +72,7 @@ namespace osu.Game.Screens.Play var convertedFrame = (ReplayFrame)convertibleFrame; convertedFrame.Time = frame.Time; + convertedFrame.Header = frame.Header; score.Replay.Frames.Add(convertedFrame); } From 0458d408bb36b15c00c47634926dea59b116205c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 31 Jan 2022 18:37:51 +0900 Subject: [PATCH 319/587] Add replay statistics frames to FramedReplayInputHandler --- .../EmptyFreeformFramedReplayInputHandler.cs | 2 +- .../PippidonFramedReplayInputHandler.cs | 2 +- .../EmptyScrollingFramedReplayInputHandler.cs | 2 +- .../PippidonFramedReplayInputHandler.cs | 2 +- .../Replays/CatchFramedReplayInputHandler.cs | 2 +- .../Replays/ManiaFramedReplayInputHandler.cs | 2 +- .../Replays/OsuFramedReplayInputHandler.cs | 2 +- .../Replays/TaikoFramedReplayInputHandler.cs | 2 +- .../Gameplay/TestSceneReplayRecorder.cs | 2 +- .../Gameplay/TestSceneSpectatorPlayback.cs | 2 +- osu.Game/Input/Handlers/ReplayInputHandler.cs | 34 +++++++++++++++++++ .../Replays/FramedReplayInputHandler.cs | 15 ++++++++ 12 files changed, 59 insertions(+), 10 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs index cc4483de31..a9bc8dc10e 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformFramedReplayInputHandler.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays protected override bool IsImportant(EmptyFreeformReplayFrame frame) => frame.Actions.Any(); - public override void CollectPendingInputs(List inputs) + protected override void CollectReplayInputs(List inputs) { var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time); diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs index e005346e1e..dbfaf8a01d 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Pippidon.Replays protected override bool IsImportant(PippidonReplayFrame frame) => true; - public override void CollectPendingInputs(List inputs) + protected override void CollectReplayInputs(List inputs) { var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time); diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs index 4b998cfca3..1d33ab8a54 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingFramedReplayInputHandler.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays protected override bool IsImportant(EmptyScrollingReplayFrame frame) => frame.Actions.Any(); - public override void CollectPendingInputs(List inputs) + protected override void CollectReplayInputs(List inputs) { inputs.Add(new ReplayState { diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs index 7652357b4d..702f6fdb04 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonFramedReplayInputHandler.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Pippidon.Replays protected override bool IsImportant(PippidonReplayFrame frame) => frame.Actions.Any(); - public override void CollectPendingInputs(List inputs) + protected override void CollectReplayInputs(List inputs) { inputs.Add(new ReplayState { diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs index bd742ce6a6..b6af88a771 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Replays protected override bool IsImportant(CatchReplayFrame frame) => frame.Actions.Any(); - public override void CollectPendingInputs(List inputs) + protected override void CollectReplayInputs(List inputs) { float position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time); diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs index aa0c148caf..aa164f95da 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Replays protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any(); - public override void CollectPendingInputs(List inputs) + protected override void CollectReplayInputs(List inputs) { inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs index 7d696dfb79..ea36ecc399 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Replays protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Any(); - public override void CollectPendingInputs(List inputs) + protected override void CollectReplayInputs(List inputs) { var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time); diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs index 138e8f9785..2f9b6c7f60 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Replays protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any(); - public override void CollectPendingInputs(List inputs) + protected override void CollectReplayInputs(List inputs) { inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 4eab1a21da..8df32c500e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -210,7 +210,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - public override void CollectPendingInputs(List inputs) + protected override void CollectReplayInputs(List inputs) { inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) }); inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 90abdf2ba3..4af254866a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -259,7 +259,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - public override void CollectPendingInputs(List inputs) + protected override void CollectReplayInputs(List inputs) { inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) }); inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); diff --git a/osu.Game/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs index e4aec4edac..205a1ea1ac 100644 --- a/osu.Game/Input/Handlers/ReplayInputHandler.cs +++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs @@ -9,6 +9,7 @@ using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges.Events; using osu.Framework.Input.States; using osu.Framework.Platform; +using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; using osuTK; @@ -79,5 +80,38 @@ namespace osu.Game.Input.Handlers PressedActions = pressedActions; } } + + /// + /// An that is triggered when a frame containing replay statistics arrives. + /// + public class ReplayStatisticsFrameInput : IInput + { + /// + /// The frame containing the statistics. + /// + public ReplayFrame Frame; + + public void Apply(InputState state, IInputStateChangeHandler handler) + { + handler.HandleInputStateChange(new ReplayStatisticsFrameEvent(state, this, Frame)); + } + } + + /// + /// An that is triggered when a frame containing replay statistics arrives. + /// + public class ReplayStatisticsFrameEvent : InputStateChangeEvent + { + /// + /// The frame containing the statistics. + /// + public readonly ReplayFrame Frame; + + public ReplayStatisticsFrameEvent(InputState state, IInput input, ReplayFrame frame) + : base(state, input) + { + Frame = frame; + } + } } } diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index d3ee10dd23..f889d15485 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Input.StateChanges; using osu.Game.Input.Handlers; using osu.Game.Replays; @@ -174,5 +175,19 @@ namespace osu.Game.Rulesets.Replays return Frames[index].Time; } + + public sealed override void CollectPendingInputs(List inputs) + { + base.CollectPendingInputs(inputs); + + CollectReplayInputs(inputs); + + if (CurrentFrame?.Header != null) + inputs.Add(new ReplayStatisticsFrameInput { Frame = CurrentFrame }); + } + + protected virtual void CollectReplayInputs(List inputs) + { + } } } From 39e1d65976ba2d35e0c7729b21ca75fdf1745864 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 31 Jan 2022 18:43:10 +0900 Subject: [PATCH 320/587] Make ScoreProcessor write all judgement types --- osu.Game/Rulesets/Scoring/HitResult.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 5599ed96a3..a254f9b760 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Scoring /// /// An array of all scorable s. /// - public static readonly HitResult[] SCORABLE_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).Where(r => r.IsScorable()).ToArray(); + public static readonly HitResult[] ALL_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).ToArray(); /// /// Whether a is valid within a given range. diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index fc24972b8e..fc5e982521 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -125,6 +125,8 @@ namespace osu.Game.Rulesets.Scoring if (result.FailedAtJudgement) return; + scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1; + if (!result.Type.IsScorable()) return; @@ -151,8 +153,6 @@ namespace osu.Game.Rulesets.Scoring rollingMaxBaseScore += result.Judgement.MaxNumericResult; } - scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1; - hitEvents.Add(CreateHitEvent(result)); lastHitObject = result.HitObject; @@ -175,6 +175,8 @@ namespace osu.Game.Rulesets.Scoring if (result.FailedAtJudgement) return; + scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1; + if (!result.Type.IsScorable()) return; @@ -186,8 +188,6 @@ namespace osu.Game.Rulesets.Scoring rollingMaxBaseScore -= result.Judgement.MaxNumericResult; } - scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1; - Debug.Assert(hitEvents.Count > 0); lastHitObject = hitEvents[^1].LastHitObject; hitEvents.RemoveAt(hitEvents.Count - 1); @@ -346,7 +346,7 @@ namespace osu.Game.Rulesets.Scoring score.Accuracy = Accuracy.Value; score.Rank = Rank.Value; - foreach (var result in HitResultExtensions.SCORABLE_TYPES) + foreach (var result in HitResultExtensions.ALL_TYPES) score.Statistics[result] = GetStatistic(result); score.HitEvents = hitEvents; From 4fb565e15f1be7d6fd740090f24cd2d98552aabb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 31 Jan 2022 18:54:23 +0900 Subject: [PATCH 321/587] Reset ScoreProcessor from statistics replay frames --- .../Gameplay/TestSceneScoreProcessor.cs | 37 +++++++++ .../Rulesets/Scoring/JudgementProcessor.cs | 12 +++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 77 +++++++++++++++++-- osu.Game/Rulesets/UI/RulesetInputManager.cs | 32 +++++--- osu.Game/Screens/Play/Player.cs | 1 + 5 files changed, 144 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index 432e3df95e..bcaa70d062 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -1,11 +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; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.Spectator; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Tests.Visual; @@ -42,6 +48,37 @@ namespace osu.Game.Tests.Gameplay Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(Judgement.LARGE_BONUS_SCORE)); } + [Test] + public void TestResetFromReplayFrame() + { + var beatmap = new Beatmap { HitObjects = { new HitCircle() } }; + + var scoreProcessor = new ScoreProcessor(); + scoreProcessor.ApplyBeatmap(beatmap); + + scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great }); + Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000)); + Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); + + // Reset with a miss instead. + scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame + { + Header = new FrameHeader(0, 0, 0, new Dictionary { { HitResult.Miss, 1 } }, DateTimeOffset.Now) + }); + + Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); + Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); + + // Reset with no judged hit. + scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame + { + Header = new FrameHeader(0, 0, 0, new Dictionary(), DateTimeOffset.Now) + }); + + Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); + Assert.That(scoreProcessor.JudgedHits, Is.Zero); + } + private class TestJudgement : Judgement { public override HitResult MaxResult { get; } diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index c3c4a2c949..f38155240e 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Scoring { @@ -107,6 +108,17 @@ namespace osu.Game.Rulesets.Scoring JudgedHits = 0; } + public virtual void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) + { + if (frame.Header == null) + return; + + JudgedHits = 0; + + foreach ((_, int count) in frame.Header.Statistics) + JudgedHits += count; + } + /// /// Creates the that represents the scoring result for a . /// diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index fc5e982521..37ee3771df 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -7,9 +7,11 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Utils; +using osu.Game.Extensions; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; using osu.Game.Scoring; namespace osu.Game.Rulesets.Scoring @@ -18,6 +20,11 @@ namespace osu.Game.Rulesets.Scoring { private const double max_score = 1000000; + /// + /// Invoked when this was reset from a replay frame. + /// + public event Action OnResetFromReplayFrame; + /// /// The current total score. /// @@ -329,12 +336,6 @@ namespace osu.Game.Rulesets.Scoring HighestCombo.Value = 0; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - hitEvents.Clear(); - } - /// /// Retrieve a score populated with data for the current play this processor is responsible for. /// @@ -351,6 +352,70 @@ namespace osu.Game.Rulesets.Scoring score.HitEvents = hitEvents; } + + /// + /// Maximum for a normal hit (i.e. not tick/bonus) for this ruleset. Only populated via . + /// + private HitResult? maxNormalResult; + + public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) + { + base.ResetFromReplayFrame(ruleset, frame); + + if (frame.Header == null) + return; + + baseScore = 0; + rollingMaxBaseScore = 0; + HighestCombo.Value = frame.Header.MaxCombo; + + foreach ((HitResult result, int count) in frame.Header.Statistics) + { + // Bonus scores are counted separately directly from the statistics dictionary later on. + if (!result.IsScorable() || result.IsBonus()) + continue; + + // The maximum result of this judgement if it wasn't a miss. + // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT). + HitResult maxResult; + + switch (result) + { + case HitResult.LargeTickHit: + case HitResult.LargeTickMiss: + maxResult = HitResult.LargeTickHit; + break; + + case HitResult.SmallTickHit: + case HitResult.SmallTickMiss: + maxResult = HitResult.SmallTickHit; + break; + + default: + maxResult = maxNormalResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result; + break; + } + + for (int i = 0; i < count; i++) + { + baseScore += Judgement.ToNumericResult(result); + rollingMaxBaseScore += Judgement.ToNumericResult(maxResult); + } + } + + scoreResultCounts.Clear(); + scoreResultCounts.AddRange(frame.Header.Statistics); + + updateScore(); + + OnResetFromReplayFrame?.Invoke(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + hitEvents.Clear(); + } } public enum ScoringMode diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 370c99ffaf..050ec82071 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -16,6 +16,7 @@ using osu.Game.Configuration; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using static osu.Game.Input.Handlers.ReplayInputHandler; @@ -24,6 +25,11 @@ namespace osu.Game.Rulesets.UI public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler where T : struct { + private readonly Ruleset ruleset; + + [Resolved(CanBeNull = true)] + private ScoreProcessor scoreProcessor { get; set; } + private ReplayRecorder recorder; public ReplayRecorder Recorder @@ -51,6 +57,8 @@ namespace osu.Game.Rulesets.UI protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) { + this.ruleset = ruleset.CreateInstance(); + InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique) .WithChild(content = new Container { RelativeSizeAxes = Axes.Both }); @@ -66,17 +74,23 @@ namespace osu.Game.Rulesets.UI public override void HandleInputStateChange(InputStateChangeEvent inputStateChange) { - if (inputStateChange is ReplayStateChangeEvent replayStateChanged) + switch (inputStateChange) { - foreach (var action in replayStateChanged.ReleasedActions) - KeyBindingContainer.TriggerReleased(action); + case ReplayStateChangeEvent stateChangeEvent: + foreach (var action in stateChangeEvent.ReleasedActions) + KeyBindingContainer.TriggerReleased(action); - foreach (var action in replayStateChanged.PressedActions) - KeyBindingContainer.TriggerPressed(action); - } - else - { - base.HandleInputStateChange(inputStateChange); + foreach (var action in stateChangeEvent.PressedActions) + KeyBindingContainer.TriggerPressed(action); + break; + + case ReplayStatisticsFrameEvent statisticsStateChangeEvent: + scoreProcessor?.ResetFromReplayFrame(ruleset, statisticsStateChangeEvent.Frame); + break; + + default: + base.HandleInputStateChange(inputStateChange); + break; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index cfca2d0a3d..e0fcca4eb7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -165,6 +165,7 @@ namespace osu.Game.Screens.Play PrepareReplay(); ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo); + ScoreProcessor.OnResetFromReplayFrame += () => ScoreProcessor.PopulateScore(Score.ScoreInfo); gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true); } From e5601772a99aba7374077f986f158a1a839f2264 Mon Sep 17 00:00:00 2001 From: Spooghetti420 Date: Mon, 31 Jan 2022 15:00:36 +0000 Subject: [PATCH 322/587] Make incompatible with --- osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs | 3 +++ osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index 42dc7fdc1f..1abca970d5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; @@ -25,6 +26,8 @@ namespace osu.Game.Rulesets.Mania.Mods public override ModType Type => ModType.Conversion; + public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) }; + public const double END_NOTE_ALLOW_THRESHOLD = 0.5; public void ApplyToBeatmap(IBeatmap beatmap) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs index 1ea45c295c..4cbdaee323 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Mania.Mods public override ModType Type => ModType.Conversion; + public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) }; + public void ApplyToBeatmap(IBeatmap beatmap) { var maniaBeatmap = (ManiaBeatmap)beatmap; From 2f88efd3c3c2f66f0bc8104f737cbc532527251f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Feb 2022 00:53:56 +0900 Subject: [PATCH 323/587] Pass column in rather than accessing parent --- osu.Game.Rulesets.Mania/UI/Column.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index a6c64e9b2d..dc3b86ac7f 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.UI }, background, TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }, - new ColumnTouchInputArea() + new ColumnTouchInputArea(this) }; hitPolicy = new OrderedHitPolicy(HitObjectContainer); @@ -143,17 +143,19 @@ namespace osu.Game.Rulesets.Mania.UI public class ColumnTouchInputArea : Drawable { + private readonly Column column; + [Resolved(canBeNull: true)] private ManiaInputManager maniaInputManager { get; set; } private KeyBindingContainer keyBindingContainer; - public ColumnTouchInputArea() + public ColumnTouchInputArea(Column column) { RelativeSizeAxes = Axes.Both; - } - private Column column => (Column)Parent; + this.column = column; + } protected override void LoadComplete() { From 9227211a441eb2df90a2e0504e6bdaa08f94bfe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Jan 2022 22:56:27 +0100 Subject: [PATCH 324/587] Privatise `shouldAlternate` --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index db8bd217f6..f68f2b5bee 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) }; public override ModType Type => ModType.DifficultyIncrease; public override IconUsage? Icon => FontAwesome.Solid.Keyboard; - public bool ShouldAlternate => !isBreakTime.Value && introEnded; private bool introEnded; private double earliestStartTime; @@ -34,6 +33,8 @@ namespace osu.Game.Rulesets.Osu.Mods private OsuAction? lastActionPressed; private DrawableRuleset ruleset; + private bool shouldAlternate => !isBreakTime.Value && introEnded; + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { ruleset = drawableRuleset; @@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Mods } public bool OnPressed(KeyBindingPressEvent e) - => mod.ShouldAlternate && mod.onPressed(e.Action); + => mod.shouldAlternate && mod.onPressed(e.Action); public void OnReleased(KeyBindingReleaseEvent e) { From c50577e25ff3d20c307cbec120b8436558cb6aa3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Feb 2022 10:48:41 +0900 Subject: [PATCH 325/587] Apply suggestion from review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 37ee3771df..79861c0ecc 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -396,11 +396,8 @@ namespace osu.Game.Rulesets.Scoring break; } - for (int i = 0; i < count; i++) - { - baseScore += Judgement.ToNumericResult(result); - rollingMaxBaseScore += Judgement.ToNumericResult(maxResult); - } + baseScore += count * Judgement.ToNumericResult(result); + rollingMaxBaseScore += count * Judgement.ToNumericResult(maxResult); } scoreResultCounts.Clear(); From 5a6d57efb77728df1d09dab9837a75e36311f611 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Feb 2022 12:43:35 +0900 Subject: [PATCH 326/587] Update fastlane and dependencies --- Gemfile.lock | 147 +++++++++++++++++++++++++++++---------------- fastlane/README.md | 2 +- 2 files changed, 95 insertions(+), 54 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8ac863c9a8..86c8baabe6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,53 +1,75 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.3) - addressable (2.7.0) + CFPropertyList (3.0.5) + rexml + addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) + artifactory (3.0.15) atomos (0.1.3) - aws-eventstream (1.1.0) - aws-partitions (1.413.0) - aws-sdk-core (3.110.0) + aws-eventstream (1.2.0) + aws-partitions (1.551.0) + aws-sdk-core (3.125.5) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) + aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.40.0) - aws-sdk-core (~> 3, >= 3.109.0) + aws-sdk-kms (1.53.0) + aws-sdk-core (~> 3, >= 3.125.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.87.0) - aws-sdk-core (~> 3, >= 3.109.0) + aws-sdk-s3 (1.111.3) + aws-sdk-core (~> 3, >= 3.125.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.1) - aws-sigv4 (1.2.2) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.4.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - claide (1.0.3) + claide (1.1.0) colored (1.2) colored2 (3.1.2) commander-fastlane (4.4.6) highline (~> 1.7.2) declarative (0.0.20) - declarative-option (0.1.0) - digest-crc (0.6.3) + digest-crc (0.6.4) rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) - emoji_regex (3.2.1) - excon (0.78.1) - faraday (1.2.0) - multipart-post (>= 1.2, < 3) - ruby2_keywords + emoji_regex (3.2.3) + excon (0.90.0) + faraday (1.9.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) faraday-cookie_jar (0.0.7) faraday (>= 0.8.0) http-cookie (~> 1.0.0) - faraday_middleware (1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.3) + multipart-post (>= 1.2, < 3) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.1) - fastlane (2.170.0) + fastimage (2.2.6) + fastlane (2.181.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) + artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) @@ -68,6 +90,7 @@ GEM jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (~> 2.0.0) + naturally (~> 2.2) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) @@ -94,65 +117,80 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.0) signet (~> 0.12) - google-cloud-core (1.5.0) + google-apis-core (0.4.2) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.10.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-storage_v1 (0.11.0) + google-apis-core (>= 0.4, < 2.a) + google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.4.0) + google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) - google-cloud-errors (1.0.1) - google-cloud-storage (1.29.2) - addressable (~> 2.5) + google-cloud-errors (1.2.0) + google-cloud-storage (1.36.0) + addressable (~> 2.8) digest-crc (~> 0.4) - google-api-client (~> 0.33) - google-cloud-core (~> 1.2) - googleauth (~> 0.9) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.1) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (0.14.0) + googleauth (0.17.1) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.14) + signet (~> 0.15) highline (1.7.10) - http-cookie (1.0.3) + http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) - jmespath (1.4.0) - json (2.5.1) - jwt (2.2.2) + jmespath (1.5.0) + json (2.6.1) + jwt (2.3.0) memoist (0.16.2) mini_magick (4.11.0) - mini_mime (1.0.2) + mini_mime (1.1.2) mini_portile2 (2.4.0) multi_json (1.15.0) multipart-post (2.0.0) nanaimo (0.3.0) - naturally (2.2.0) + naturally (2.2.1) nokogiri (1.10.10) mini_portile2 (~> 2.4.0) - os (1.1.1) - plist (3.5.0) + os (1.1.4) + plist (3.6.0) public_suffix (4.0.6) - rake (13.0.3) - representable (3.0.4) + rake (13.0.6) + representable (3.1.1) declarative (< 0.1.0) - declarative-option (< 0.2.0) + trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) + rexml (3.2.5) rouge (2.0.7) - ruby2_keywords (0.0.2) - rubyzip (2.3.0) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) security (0.1.3) - signet (0.14.0) - addressable (~> 2.3) + signet (0.16.0) + addressable (~> 2.8) faraday (>= 0.17.3, < 2.0) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) simctl (1.6.8) CFPropertyList naturally - slack-notifier (2.3.2) + slack-notifier (2.4.0) souyuz (0.9.1) fastlane (>= 1.103.0) highline (~> 1.7) @@ -160,6 +198,7 @@ GEM terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) + trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) tty-spinner (0.9.3) @@ -167,18 +206,20 @@ GEM uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.7) - unicode-display_width (1.7.0) + unf_ext (0.0.8) + unicode-display_width (1.8.0) + webrick (1.7.0) word_wrap (1.0.0) - xcodeproj (1.19.0) + xcodeproj (1.21.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) + rexml (~> 3.2.4) xcpretty (0.3.0) rouge (~> 2.0.7) - xcpretty-travis-formatter (1.0.0) + xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) PLATFORMS diff --git a/fastlane/README.md b/fastlane/README.md index a400ed9516..8273fdaa5d 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -12,7 +12,7 @@ Install _fastlane_ using ``` [sudo] gem install fastlane -NV ``` -or alternatively using `brew cask install fastlane` +or alternatively using `brew install fastlane` # Available Actions ## Android From aa492270dd9738bf9cf8c7dc0c4eb7543e155f8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Feb 2022 13:30:26 +0900 Subject: [PATCH 327/587] Ignore `FodyWeavers.xml` files in subdirectories These are created when building specific projects rather than the main solution (typically iOS / android) and of no use to us. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index de6a3ac848..5b19270ab9 100644 --- a/.gitignore +++ b/.gitignore @@ -339,3 +339,4 @@ inspectcode # Fody (pulled in by Realm) - schema file FodyWeavers.xsd +**/FodyWeavers.xml From db973fb34876e1329c215f08dcee2dc7039ae422 Mon Sep 17 00:00:00 2001 From: dekrain Date: Tue, 1 Feb 2022 06:28:18 +0100 Subject: [PATCH 328/587] Add basic tooltip for leaderboard scores --- .../Online/Leaderboards/LeaderboardScore.cs | 5 +- .../Leaderboards/LeaderboardScoreTooltip.cs | 225 ++++++++++++++++++ 2 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 906e09b8c1..7779043ccd 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -32,7 +32,7 @@ using osu.Game.Utils; namespace osu.Game.Online.Leaderboards { - public class LeaderboardScore : OsuClickableContainer, IHasContextMenu + public class LeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { public const float HEIGHT = 60; @@ -70,6 +70,9 @@ namespace osu.Game.Online.Leaderboards [Resolved] private Storage storage { get; set; } + public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); + public ScoreInfo TooltipContent => Score; + public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true) { Score = score; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs new file mode 100644 index 0000000000..78064c33bc --- /dev/null +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -0,0 +1,225 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Game.Scoring; +using osuTK; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Online.Leaderboards +{ + public class LeaderboardScoreTooltip : VisibilityContainer, ITooltip + { + private OsuSpriteText timestampLabel; + private FillFlowContainer topScoreStatistics; + private FillFlowContainer bottomScoreStatistics; + private FillFlowContainer modStatistics; + + public LeaderboardScoreTooltip() + { + AutoSizeAxes = Axes.Both; + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.7f, + Colour = Colour4.Black, + }, + new GridContainer + { + Margin = new MarginPadding(5f), + AutoSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + // Info row + new Drawable[] + { + timestampLabel = new OsuSpriteText() + { + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + } + }, + // Mods row + new Drawable[] + { + modStatistics = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + } + }, + // Actual stats rows + new Drawable[] + { + topScoreStatistics = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + } + }, + new Drawable[] + { + bottomScoreStatistics = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + } + }, + } + } + }; + } + + private ScoreInfo currentScore; + + public void SetContent(ScoreInfo score) + { + if (currentScore == score) + return; + currentScore = score; + + timestampLabel.Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}"; + + modStatistics.Clear(); + topScoreStatistics.Clear(); + bottomScoreStatistics.Clear(); + + foreach (var mod in score.Mods) + { + modStatistics.Add(new ModCell(mod)); + } + + foreach (var result in score.GetStatisticsForDisplay()) + { + (result.Result > HitResult.Perfect + ? bottomScoreStatistics + : topScoreStatistics + ).Add(new HitResultCell(result)); + } + } + + protected override void PopIn() => this.FadeIn(20, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(80, Easing.OutQuint); + + public void Move(Vector2 pos) => Position = pos; + + private class HitResultCell : CompositeDrawable + { + readonly private string DisplayName; + readonly private HitResult Result; + readonly private int Count; + + public HitResultCell(HitResultDisplayStatistic stat) + { + AutoSizeAxes = Axes.Both; + Padding = new MarginPadding{ Horizontal = 5f }; + + DisplayName = stat.DisplayName; + Result = stat.Result; + Count = stat.Count; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + InternalChild = new FillFlowContainer + { + Height = 12, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2f, 0f), + Children = new Drawable[] + { + new CircularContainer + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#222") + }, + new OsuSpriteText + { + Padding = new MarginPadding{ Horizontal = 2f }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + Text = DisplayName.ToUpperInvariant(), + Colour = colours.ForHitResult(Result), + } + } + }, + new OsuSpriteText + { + RelativeSizeAxes = Axes.Y, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Text = Count.ToString(), + }, + } + }; + } + } + + private class ModCell : CompositeDrawable + { + readonly private Mod Mod; + + public ModCell(Mod mod) + { + AutoSizeAxes = Axes.Both; + Padding = new MarginPadding{ Horizontal = 5f }; + Mod = mod; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + Height = 15, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2f, 0f), + Children = new Drawable[] + { + new ModIcon(Mod, showTooltip: false).With(icon => + { + icon.Scale = new Vector2(15f / icon.Height); + }), + new OsuSpriteText + { + RelativeSizeAxes = Axes.Y, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Text = Mod.SettingDescription, + } + } + }; + } + } + } +} From 502e6af008757204e53f04eb5cbfe7f586558989 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 00:21:54 +0900 Subject: [PATCH 329/587] Remove PlayingUsers list from SpectatorClient --- .../Gameplay/TestSceneSpectatorPlayback.cs | 38 ------------------- ...TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 10 ----- .../Dashboard/CurrentlyPlayingDisplay.cs | 30 +++++++-------- .../Visual/Spectator/TestSpectatorClient.cs | 7 ++-- 5 files changed, 19 insertions(+), 68 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 90abdf2ba3..75369661a0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -3,12 +3,9 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -20,7 +17,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; @@ -47,8 +43,6 @@ namespace osu.Game.Tests.Visual.Gameplay private Replay replay; - private readonly IBindableList users = new BindableList(); - private TestReplayRecorder recorder; private ManualClock manualClock; @@ -57,9 +51,6 @@ namespace osu.Game.Tests.Visual.Gameplay private TestFramedReplayInputHandler replayHandler; - [Resolved] - private IAPIProvider api { get; set; } - [Resolved] private SpectatorClient spectatorClient { get; set; } @@ -78,35 +69,6 @@ namespace osu.Game.Tests.Visual.Gameplay spectatorClient.OnNewFrames += onNewFrames; - users.BindTo(spectatorClient.PlayingUsers); - users.BindCollectionChanged((obj, args) => - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - Debug.Assert(args.NewItems != null); - - foreach (int user in args.NewItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.WatchUser(user); - } - - break; - - case NotifyCollectionChangedAction.Remove: - Debug.Assert(args.OldItems != null); - - foreach (int user in args.OldItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.StopWatchingUser(user); - } - - break; - } - }, true); - Children = new Drawable[] { new GridContainer diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 9b8e67b07a..8b71dfac21 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void RandomlyUpdateState() { - foreach (int userId in PlayingUsers) + foreach ((int userId, _) in PlayingUserStates) { if (RNG.NextBool()) continue; diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index fddb94fad7..c3e70edcd3 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -37,9 +37,6 @@ namespace osu.Game.Online.Spectator private readonly List watchingUsers = new List(); - public IBindableList PlayingUsers => playingUsers; - private readonly BindableList playingUsers = new BindableList(); - public IBindableDictionary PlayingUserStates => playingUserStates; private readonly BindableDictionary playingUserStates = new BindableDictionary(); @@ -88,10 +85,7 @@ namespace osu.Game.Online.Spectator BeginPlayingInternal(currentState); } else - { - playingUsers.Clear(); playingUserStates.Clear(); - } }), true); } @@ -99,9 +93,6 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - if (!playingUsers.Contains(userId)) - playingUsers.Add(userId); - // UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched. // This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29). // We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations. @@ -118,7 +109,6 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - playingUsers.Remove(userId); playingUserStates.Remove(userId); OnUserFinishedPlaying?.Invoke(userId, state); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index fde20575fc..afbfb9f7ef 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private readonly IBindableList playingUsers = new BindableList(); + private readonly IBindableDictionary playingUserStates = new BindableDictionary(); private FillFlowContainer userFlow; @@ -51,18 +51,20 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - playingUsers.BindTo(spectatorClient.PlayingUsers); - playingUsers.BindCollectionChanged(onUsersChanged, true); + playingUserStates.BindTo(spectatorClient.PlayingUserStates); + playingUserStates.BindCollectionChanged(onUsersChanged, true); } - private void onUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => + private void onUsersChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { - case NotifyCollectionChangedAction.Add: - foreach (int id in e.NewItems.OfType().ToArray()) + case NotifyDictionaryChangedAction.Add: + Debug.Assert(e.NewItems != null); + + foreach ((int userId, _) in e.NewItems) { - users.GetUserAsync(id).ContinueWith(task => + users.GetUserAsync(userId).ContinueWith(task => { var user = task.GetResultSafely(); @@ -71,7 +73,7 @@ namespace osu.Game.Overlays.Dashboard Schedule(() => { // user may no longer be playing. - if (!playingUsers.Contains(user.Id)) + if (!playingUserStates.ContainsKey(user.Id)) return; userFlow.Add(createUserPanel(user)); @@ -81,13 +83,11 @@ namespace osu.Game.Overlays.Dashboard break; - case NotifyCollectionChangedAction.Remove: - foreach (int u in e.OldItems.OfType()) - userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire(); - break; + case NotifyDictionaryChangedAction.Remove: + Debug.Assert(e.OldItems != null); - case NotifyCollectionChangedAction.Reset: - userFlow.Clear(); + foreach ((int userId, _) in e.OldItems) + userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); break; } }); diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 1a1d493249..7848a825f4 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -42,7 +41,7 @@ namespace osu.Game.Tests.Visual.Spectator { OnNewFrames += (i, bundle) => { - if (PlayingUsers.Contains(i)) + if (PlayingUserStates.ContainsKey(i)) lastReceivedUserFrames[i] = bundle.Frames[^1]; }; } @@ -65,7 +64,7 @@ namespace osu.Game.Tests.Visual.Spectator /// The user to end play for. public void EndPlay(int userId) { - if (!PlayingUsers.Contains(userId)) + if (!PlayingUserStates.ContainsKey(userId)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState @@ -131,7 +130,7 @@ namespace osu.Game.Tests.Visual.Spectator protected override Task WatchUserInternal(int userId) { // When newly watching a user, the server sends the playing state immediately. - if (PlayingUsers.Contains(userId)) + if (PlayingUserStates.ContainsKey(userId)) sendPlayingState(userId); return Task.CompletedTask; From c2b775c0a39cebce5cf2d2494266484f17223094 Mon Sep 17 00:00:00 2001 From: dekrain Date: Tue, 1 Feb 2022 06:45:59 +0100 Subject: [PATCH 330/587] Minor alignment adjustments --- .../Leaderboards/LeaderboardScoreTooltip.cs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index 78064c33bc..bea589e47f 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -199,7 +199,8 @@ namespace osu.Game.Online.Leaderboards [BackgroundDependencyLoader] private void load() { - InternalChild = new FillFlowContainer + FillFlowContainer container; + InternalChild = container = new FillFlowContainer { Height = 15, AutoSizeAxes = Axes.X, @@ -209,16 +210,25 @@ namespace osu.Game.Online.Leaderboards { new ModIcon(Mod, showTooltip: false).With(icon => { + icon.Origin = Anchor.CentreLeft; + icon.Anchor = Anchor.CentreLeft; icon.Scale = new Vector2(15f / icon.Height); }), - new OsuSpriteText - { - RelativeSizeAxes = Axes.Y, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = Mod.SettingDescription, - } } }; + + string description = Mod.SettingDescription; + if (!string.IsNullOrEmpty(description)) + { + container.Add(new OsuSpriteText + { + RelativeSizeAxes = Axes.Y, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Text = Mod.SettingDescription, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + }); + } } } } From 781cb9f18d55e21b153967ee389c1a45ca934e0a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 01:45:11 +0900 Subject: [PATCH 331/587] Move HasPassed/HasFailed into GameplayState --- .../TestSceneTaikoSuddenDeath.cs | 2 +- .../Visual/Gameplay/TestSceneFailAnimation.cs | 2 +- .../Visual/Gameplay/TestSceneFailJudgement.cs | 2 +- .../Visual/Gameplay/TestScenePause.cs | 12 ++++++------ .../TestScenePlayerScoreSubmission.cs | 4 ++-- .../Gameplay/TestSceneStoryboardWithOutro.cs | 2 +- .../Navigation/TestSceneScreenNavigation.cs | 2 +- .../Multiplayer/MultiplayerPlayerLoader.cs | 2 +- osu.Game/Screens/Play/GameplayState.cs | 10 ++++++++++ osu.Game/Screens/Play/Player.cs | 19 ++++++------------- 10 files changed, 30 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 0be005e1c4..eec88d7bf8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Player.ScoreProcessor.NewJudgement += b => judged = true; }); AddUntilStep("swell judged", () => judged); - AddAssert("failed", () => Player.HasFailed); + AddAssert("failed", () => Player.GameplayState.HasFailed); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index 7167d3120a..744227c55e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible); // The pause screen and fail animation both ramp frequency. diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index fa27e1abdd..6430c29dfa 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); AddAssert("total number of results == 1", () => { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 04676f656f..ea0255ab76 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestPauseAfterFail() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayAfterFailAnimation() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayDuringFailAnimation() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); // will finish the fail animation and show the fail/pause screen. AddStep("attempt exit via pause key", () => Player.ExitViaPause()); @@ -227,7 +227,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickRetryFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); @@ -236,7 +236,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickExitFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); @@ -341,7 +341,7 @@ namespace osu.Game.Tests.Visual.Gameplay { confirmClockRunning(false); confirmNotExited(); - AddAssert("player not failed", () => !Player.HasFailed); + AddAssert("player not failed", () => !Player.GameplayState.HasFailed); AddAssert("pause overlay shown", () => Player.PauseOverlayVisible); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index a9675a2ee2..58b5df2612 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for token request", () => Player.TokenCreationRequested); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("exit", () => Player.Exit()); AddAssert("ensure no submission", () => Player.SubmittedScore == null); @@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.Gameplay addFakeHit(); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("exit", () => Player.Exit()); AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 69798dcb82..b87183cbc7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600); }); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index ed484e03f6..63984a1bed 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -209,7 +209,7 @@ namespace osu.Game.Tests.Visual.Navigation return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); - AddUntilStep("wait for fail", () => player.HasFailed); + AddUntilStep("wait for fail", () => player.GameplayState.HasFailed); AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying); AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs index 470ba59a76..772651727e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class MultiplayerPlayerLoader : PlayerLoader { - public bool GameplayPassed => player?.GameplayPassed == true; + public bool GameplayPassed => player?.GameplayState.HasPassed == true; private Player player; diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 83881f739d..60fbfa6644 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -39,6 +39,16 @@ namespace osu.Game.Screens.Play /// public readonly Score Score; + /// + /// Whether gameplay completed without the user failing. + /// + public bool HasPassed { get; set; } + + /// + /// Whether the user failed during gameplay. + /// + public bool HasFailed { get; set; } + /// /// A bindable tracking the last judgement result applied to any hit object. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 0312789b12..814aacd2fa 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -72,15 +72,8 @@ namespace osu.Game.Screens.Play /// protected virtual bool PauseOnFocusLost => true; - /// - /// Whether gameplay has completed without the user having failed. - /// - public bool GameplayPassed { get; private set; } - public Action RestartRequested; - public bool HasFailed { get; private set; } - private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -559,7 +552,7 @@ namespace osu.Game.Screens.Play if (showDialogFirst && !pauseOrFailDialogVisible) { // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). - if (ValidForResume && HasFailed) + if (ValidForResume && GameplayState.HasFailed) { failAnimationLayer.FinishTransforms(true); return; @@ -678,7 +671,7 @@ namespace osu.Game.Screens.Play resultsDisplayDelegate?.Cancel(); resultsDisplayDelegate = null; - GameplayPassed = false; + GameplayState.HasPassed = false; ValidForResume = true; skipOutroOverlay.Hide(); return; @@ -688,7 +681,7 @@ namespace osu.Game.Screens.Play if (HealthProcessor.HasFailed) return; - GameplayPassed = true; + GameplayState.HasPassed = true; // Setting this early in the process means that even if something were to go wrong in the order of events following, there // is no chance that a user could return to the (already completed) Player instance from a child screen. @@ -804,7 +797,7 @@ namespace osu.Game.Screens.Play if (!CheckModsAllowFailure()) return false; - HasFailed = true; + GameplayState.HasFailed = true; Score.ScoreInfo.Passed = false; // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer) @@ -859,13 +852,13 @@ namespace osu.Game.Screens.Play // replays cannot be paused and exit immediately && !DrawableRuleset.HasReplayLoaded.Value // cannot pause if we are already in a fail state - && !HasFailed; + && !GameplayState.HasFailed; private bool canResume => // cannot resume from a non-paused state GameplayClockContainer.IsPaused.Value // cannot resume if we are already in a fail state - && !HasFailed + && !GameplayState.HasFailed // already resuming && !IsResuming; From 38e075c522f72371742e4d02a948f7a53a0aac5d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 02:02:31 +0900 Subject: [PATCH 332/587] Add HasQuit gameplay state --- osu.Game/Screens/Play/GameplayState.cs | 5 +++++ osu.Game/Screens/Play/Player.cs | 3 +++ 2 files changed, 8 insertions(+) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 60fbfa6644..64e873b3bb 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -49,6 +49,11 @@ namespace osu.Game.Screens.Play /// public bool HasFailed { get; set; } + /// + /// Whether the user quit gameplay without either having either passed or failed. + /// + public bool HasQuit { get; set; } + /// /// A bindable tracking the last judgement result applied to any hit object. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 814aacd2fa..f6a2310826 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -983,6 +983,9 @@ namespace osu.Game.Screens.Play public override bool OnExiting(IScreen next) { + if (!GameplayState.HasPassed && !GameplayState.HasFailed) + GameplayState.HasQuit = true; + screenSuspension?.RemoveAndDisposeImmediately(); failAnimationLayer?.RemoveFilters(); From fd287e06f2bbe0f1115961184f837f0cd30927fe Mon Sep 17 00:00:00 2001 From: dekrain Date: Tue, 1 Feb 2022 06:51:00 +0100 Subject: [PATCH 333/587] Add missing license header --- osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index bea589e47f..ea02c238f3 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Containers; From d7b939277e81d99eb36aeb25f9aa71846b8b90c6 Mon Sep 17 00:00:00 2001 From: dekrain Date: Tue, 1 Feb 2022 07:10:00 +0100 Subject: [PATCH 334/587] Code quality improvements --- .../Leaderboards/LeaderboardScoreTooltip.cs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index ea02c238f3..0b2de3e2c8 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -19,10 +19,10 @@ namespace osu.Game.Online.Leaderboards { public class LeaderboardScoreTooltip : VisibilityContainer, ITooltip { - private OsuSpriteText timestampLabel; - private FillFlowContainer topScoreStatistics; - private FillFlowContainer bottomScoreStatistics; - private FillFlowContainer modStatistics; + private readonly OsuSpriteText timestampLabel; + private readonly FillFlowContainer topScoreStatistics; + private readonly FillFlowContainer bottomScoreStatistics; + private readonly FillFlowContainer modStatistics; public LeaderboardScoreTooltip() { @@ -58,7 +58,7 @@ namespace osu.Game.Online.Leaderboards // Info row new Drawable[] { - timestampLabel = new OsuSpriteText() + timestampLabel = new OsuSpriteText { Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), } @@ -98,8 +98,9 @@ namespace osu.Game.Online.Leaderboards public void SetContent(ScoreInfo score) { - if (currentScore == score) + if (currentScore.Equals(score)) return; + currentScore = score; timestampLabel.Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}"; @@ -116,9 +117,9 @@ namespace osu.Game.Online.Leaderboards foreach (var result in score.GetStatisticsForDisplay()) { (result.Result > HitResult.Perfect - ? bottomScoreStatistics - : topScoreStatistics - ).Add(new HitResultCell(result)); + ? bottomScoreStatistics + : topScoreStatistics + ).Add(new HitResultCell(result)); } } @@ -129,9 +130,9 @@ namespace osu.Game.Online.Leaderboards private class HitResultCell : CompositeDrawable { - readonly private string DisplayName; - readonly private HitResult Result; - readonly private int Count; + private readonly string DisplayName; + private readonly HitResult Result; + private readonly int Count; public HitResultCell(HitResultDisplayStatistic stat) { @@ -190,7 +191,7 @@ namespace osu.Game.Online.Leaderboards private class ModCell : CompositeDrawable { - readonly private Mod Mod; + private readonly Mod Mod; public ModCell(Mod mod) { From e1b57c4bf61d97070021f154a25cbd802b3133dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Feb 2022 16:07:57 +0900 Subject: [PATCH 335/587] Fix inspections --- .../Leaderboards/LeaderboardScoreTooltip.cs | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index 0b2de3e2c8..7825a50a0b 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -119,7 +119,7 @@ namespace osu.Game.Online.Leaderboards (result.Result > HitResult.Perfect ? bottomScoreStatistics : topScoreStatistics - ).Add(new HitResultCell(result)); + ).Add(new HitResultCell(result)); } } @@ -130,18 +130,18 @@ namespace osu.Game.Online.Leaderboards private class HitResultCell : CompositeDrawable { - private readonly string DisplayName; - private readonly HitResult Result; - private readonly int Count; + private readonly string displayName; + private readonly HitResult result; + private readonly int count; public HitResultCell(HitResultDisplayStatistic stat) { AutoSizeAxes = Axes.Both; - Padding = new MarginPadding{ Horizontal = 5f }; + Padding = new MarginPadding { Horizontal = 5f }; - DisplayName = stat.DisplayName; - Result = stat.Result; - Count = stat.Count; + displayName = stat.DisplayName; + result = stat.Result; + count = stat.Count; } [BackgroundDependencyLoader] @@ -169,12 +169,12 @@ namespace osu.Game.Online.Leaderboards }, new OsuSpriteText { - Padding = new MarginPadding{ Horizontal = 2f }, + Padding = new MarginPadding { Horizontal = 2f }, Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), - Text = DisplayName.ToUpperInvariant(), - Colour = colours.ForHitResult(Result), + Text = displayName.ToUpperInvariant(), + Colour = colours.ForHitResult(result), } } }, @@ -182,7 +182,7 @@ namespace osu.Game.Online.Leaderboards { RelativeSizeAxes = Axes.Y, Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = Count.ToString(), + Text = count.ToString(), }, } }; @@ -191,13 +191,13 @@ namespace osu.Game.Online.Leaderboards private class ModCell : CompositeDrawable { - private readonly Mod Mod; + private readonly Mod mod; public ModCell(Mod mod) { AutoSizeAxes = Axes.Both; - Padding = new MarginPadding{ Horizontal = 5f }; - Mod = mod; + Padding = new MarginPadding { Horizontal = 5f }; + this.mod = mod; } [BackgroundDependencyLoader] @@ -212,7 +212,7 @@ namespace osu.Game.Online.Leaderboards Spacing = new Vector2(2f, 0f), Children = new Drawable[] { - new ModIcon(Mod, showTooltip: false).With(icon => + new ModIcon(mod, showTooltip: false).With(icon => { icon.Origin = Anchor.CentreLeft; icon.Anchor = Anchor.CentreLeft; @@ -221,14 +221,15 @@ namespace osu.Game.Online.Leaderboards } }; - string description = Mod.SettingDescription; + string description = mod.SettingDescription; + if (!string.IsNullOrEmpty(description)) { container.Add(new OsuSpriteText { RelativeSizeAxes = Axes.Y, Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = Mod.SettingDescription, + Text = mod.SettingDescription, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }); From 855135c51e63f8a2095e7ae5dc8402a5df1b1920 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Feb 2022 16:13:27 +0900 Subject: [PATCH 336/587] Fix potential nullref during display due to incorrect equality check --- osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index 7825a50a0b..85db59b3ea 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -15,6 +15,8 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; +#nullable enable + namespace osu.Game.Online.Leaderboards { public class LeaderboardScoreTooltip : VisibilityContainer, ITooltip @@ -94,14 +96,14 @@ namespace osu.Game.Online.Leaderboards }; } - private ScoreInfo currentScore; + private ScoreInfo? displayedScore; public void SetContent(ScoreInfo score) { - if (currentScore.Equals(score)) + if (displayedScore?.Equals(score) == true) return; - currentScore = score; + displayedScore = score; timestampLabel.Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}"; From fdb52a8fd70105516a2b4fb32cb6b3b4657d98ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Feb 2022 16:20:40 +0900 Subject: [PATCH 337/587] Remove gap in tooltip display between statistics --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 7779043ccd..b4aaebd4e4 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -186,7 +186,6 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(10f, 0f), Margin = new MarginPadding { Left = edge_margin }, Children = statisticsLabels }, @@ -231,7 +230,6 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.BottomRight, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(1), ChildrenEnumerable = Score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) }) }, }, @@ -316,6 +314,7 @@ namespace osu.Game.Online.Leaderboards { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Right = 10 }, Children = new Drawable[] { new Container From 3ca2c906842209fe36e6d67ee7ace35f2cfad747 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Feb 2022 16:35:05 +0900 Subject: [PATCH 338/587] Add test scores in `BeatmapLeaderboard` test scene with more mods --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 48230ff9e9..7292ec96ed 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -197,7 +197,22 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, - //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Mods = new Mod[] + { + new OsuModHidden(), + new OsuModHardRock(), + new OsuModFlashlight + { + FollowDelay = { Value = 200 } + }, + new OsuModDifficultyAdjust + { + CircleSize = { Value = 8 }, + ApproachRate = { Value = 7 }, + OverallDifficulty = { Value = 6 }, + DrainRate = { Value = 5 }, + } + }, Ruleset = new OsuRuleset().RulesetInfo, BeatmapInfo = beatmapInfo, User = new APIUser @@ -217,7 +232,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, - //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser @@ -237,7 +252,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, - //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -258,7 +273,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, - //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -279,7 +294,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, - //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -300,7 +315,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.9826, MaxCombo = 244, TotalScore = 1707827, - //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -321,7 +336,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.9654, MaxCombo = 244, TotalScore = 1707827, - //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -342,7 +357,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.6025, MaxCombo = 244, TotalScore = 1707827, - //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -363,7 +378,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.5140, MaxCombo = 244, TotalScore = 1707827, - //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -384,7 +399,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.4222, MaxCombo = 244, TotalScore = 1707827, - //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, From 8eace12fe3b5b585657cfec9ad733c129dd4d73a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Feb 2022 16:35:22 +0900 Subject: [PATCH 339/587] Synchronise (roughly) backgrounds of all custom tooltips --- .../Drawables/DifficultyIconTooltip.cs | 1 + .../Leaderboards/LeaderboardScoreTooltip.cs | 29 ++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs index ec4bcbd65f..aba01a1294 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs @@ -30,6 +30,7 @@ namespace osu.Game.Beatmaps.Drawables { background = new Box { + Alpha = 0.9f, RelativeSizeAxes = Axes.Both }, new FillFlowContainer diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index 85db59b3ea..7b88c0d534 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -21,24 +21,31 @@ namespace osu.Game.Online.Leaderboards { public class LeaderboardScoreTooltip : VisibilityContainer, ITooltip { - private readonly OsuSpriteText timestampLabel; - private readonly FillFlowContainer topScoreStatistics; - private readonly FillFlowContainer bottomScoreStatistics; - private readonly FillFlowContainer modStatistics; + private OsuSpriteText timestampLabel = null!; + private FillFlowContainer topScoreStatistics = null!; + private FillFlowContainer bottomScoreStatistics = null!; + private FillFlowContainer modStatistics = null!; public LeaderboardScoreTooltip() { AutoSizeAxes = Axes.Both; + AutoSizeDuration = 200; + AutoSizeEasing = Easing.OutQuint; + Masking = true; CornerRadius = 5; + } + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { InternalChildren = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Alpha = 0.7f, - Colour = Colour4.Black, + Alpha = 0.9f, + Colour = colours.Gray3, }, new GridContainer { @@ -118,10 +125,10 @@ namespace osu.Game.Online.Leaderboards foreach (var result in score.GetStatisticsForDisplay()) { - (result.Result > HitResult.Perfect - ? bottomScoreStatistics - : topScoreStatistics - ).Add(new HitResultCell(result)); + if (result.Result > HitResult.Perfect) + bottomScoreStatistics.Add(new HitResultCell(result)); + else + topScoreStatistics.Add(new HitResultCell(result)); } } @@ -171,7 +178,7 @@ namespace osu.Game.Online.Leaderboards }, new OsuSpriteText { - Padding = new MarginPadding { Horizontal = 2f }, + Padding = new MarginPadding(2), Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), From f87920cd83d060542ac77784fdefef09f9521348 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Feb 2022 16:43:26 +0900 Subject: [PATCH 340/587] Remove unnecessary `GridContainer` and list mods verticall to give more space --- .../Leaderboards/LeaderboardScoreTooltip.cs | 61 ++++++++----------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index 7b88c0d534..8c349d56e0 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -47,55 +47,42 @@ namespace osu.Game.Online.Leaderboards Alpha = 0.9f, Colour = colours.Gray3, }, - new GridContainer + new FillFlowContainer { Margin = new MarginPadding(5f), + Spacing = new Vector2(10), AutoSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - }, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] + Direction = FillDirection.Vertical, + Children = new Drawable[] { // Info row - new Drawable[] + timestampLabel = new OsuSpriteText { - timestampLabel = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - } + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), }, // Mods row - new Drawable[] + modStatistics = new FillFlowContainer { - modStatistics = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - } + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, }, - // Actual stats rows - new Drawable[] + new FillFlowContainer { - topScoreStatistics = new FillFlowContainer + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - } - }, - new Drawable[] - { - bottomScoreStatistics = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, + // Actual stats rows + topScoreStatistics = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + }, + bottomScoreStatistics = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + }, } }, } From 0f83f77d2b3fc8de0d615361e53130f5958c94c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Feb 2022 16:52:53 +0900 Subject: [PATCH 341/587] Add xmldoc for new `ResetFromReplayFrame` method --- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index f38155240e..a643c31daa 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -108,6 +108,14 @@ namespace osu.Game.Rulesets.Scoring JudgedHits = 0; } + /// + /// Reset all statistics based on header information contained within a replay frame. + /// + /// + /// If the provided replay frame does not have any header information, this will be a noop. + /// + /// The ruleset to be used for retrieving statistics. + /// The replay frame to read header statistics from. public virtual void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) { if (frame.Header == null) From 15479ae046ebf96b22367565333622a573dd59ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Feb 2022 16:55:28 +0900 Subject: [PATCH 342/587] Add test coverage of no header doing nothing --- osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index bcaa70d062..70ba868de6 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -60,6 +60,12 @@ namespace osu.Game.Tests.Gameplay Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000)); Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); + // No header shouldn't cause any change + scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame()); + + Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000)); + Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); + // Reset with a miss instead. scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame { From bfe6218ed51c6451f85046998e225de2bdcbe7e6 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Tue, 1 Feb 2022 12:43:58 +0100 Subject: [PATCH 343/587] Change default orientation to SensorLandscape --- osu.Android/GameplayScreenRotationLocker.cs | 2 +- osu.Android/OsuGameActivity.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index 25bd659a5d..ab91b4e3b3 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -27,7 +27,7 @@ namespace osu.Android { gameActivity.RunOnUiThread(() => { - gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser; + gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : OsuGameActivity.DEFAULT_ORIENTATION; }); } } diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index c9fb539d8a..e6679b61a6 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -17,7 +17,7 @@ using osu.Game.Database; namespace osu.Android { - [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser)] + [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true, ScreenOrientation = DEFAULT_ORIENTATION)] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")] @@ -39,6 +39,8 @@ namespace osu.Android [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity { + public const ScreenOrientation DEFAULT_ORIENTATION = ScreenOrientation.SensorLandscape; + private static readonly string[] osu_url_schemes = { "osu", "osump" }; private OsuGameAndroid game; From 41007169f722febe2217ff805cf95cdb6d81c3dd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Feb 2022 15:51:41 +0900 Subject: [PATCH 344/587] Give SpectatorState a user state --- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../Online/Spectator/SpectatingUserState.cs | 33 +++++++++++++++++++ osu.Game/Online/Spectator/SpectatorClient.cs | 10 +++++- osu.Game/Online/Spectator/SpectatorState.cs | 7 ++-- osu.Game/Rulesets/UI/ReplayRecorder.cs | 4 ++- osu.Game/Screens/Play/Player.cs | 2 +- 6 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Online/Spectator/SpectatingUserState.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 917b3c89a8..86d6de6975 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("send frames and finish play", () => { spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero)); - spectatorClient.EndPlaying(); + spectatorClient.EndPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()) { HasPassed = true }); }); // We can't access API because we're an "online" test. diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatingUserState.cs new file mode 100644 index 0000000000..c7ba4ba248 --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatingUserState.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.Spectator +{ + public enum SpectatingUserState + { + /// + /// The spectated user has not yet played. + /// + Idle, + + /// + /// The spectated user is currently playing. + /// + Playing, + + /// + /// The spectated user has successfully completed gameplay. + /// + Completed, + + /// + /// The spectator user has failed during gameplay. + /// + Failed, + + /// + /// The spectated user has quit during gameplay. + /// + Quit + } +} diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index c3e70edcd3..f4161e1db9 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -138,6 +138,7 @@ namespace osu.Game.Online.Spectator currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); + currentState.State = SpectatingUserState.Playing; currentBeatmap = state.Beatmap; currentScore = score; @@ -148,7 +149,7 @@ namespace osu.Game.Online.Spectator public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data); - public void EndPlaying() + public void EndPlaying(GameplayState state) { // This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue). // We probably need to find a better way to handle this... @@ -163,6 +164,13 @@ namespace osu.Game.Online.Spectator IsPlaying = false; currentBeatmap = null; + if (state.HasPassed) + currentState.State = SpectatingUserState.Completed; + else if (state.HasFailed) + currentState.State = SpectatingUserState.Failed; + else + currentState.State = SpectatingUserState.Quit; + EndPlayingInternal(currentState); }); } diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index ebb91e4dd2..fc62f16bba 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -24,14 +24,17 @@ namespace osu.Game.Online.Spectator [Key(2)] public IEnumerable Mods { get; set; } = Enumerable.Empty(); + [Key(3)] + public SpectatingUserState State { get; set; } + public bool Equals(SpectatorState other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID; + return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID && State == other.State; } - public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; + public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID} State:{State}"; } } diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 976f95cef8..277040b2a6 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -55,7 +55,9 @@ namespace osu.Game.Rulesets.UI protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - spectatorClient?.EndPlaying(); + + if (spectatorClient != null && gameplayState != null) + spectatorClient.EndPlaying(gameplayState); } protected override void Update() diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f6a2310826..1aa6cded48 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1000,7 +1000,7 @@ namespace osu.Game.Screens.Play // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. // To resolve test failures, forcefully end playing synchronously when this screen exits. // Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method. - spectatorClient.EndPlaying(); + spectatorClient.EndPlaying(GameplayState); // GameplayClockContainer performs seeks / start / stop operations on the beatmap's track. // as we are no longer the current screen, we cannot guarantee the track is still usable. From f4210f7a302a3bad2130181800106cdcefeb2020 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 22:21:07 +0900 Subject: [PATCH 345/587] Rework spectator components to use new user state --- osu.Game/Online/Spectator/SpectatorClient.cs | 8 +--- .../Dashboard/CurrentlyPlayingDisplay.cs | 42 ++++++++++++------- osu.Game/Screens/Spectate/SpectatorScreen.cs | 33 ++++++++------- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index f4161e1db9..fb7526347b 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -93,12 +93,8 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - // UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched. - // This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29). - // We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations. if (watchingUsers.Contains(userId)) playingUserStates[userId] = state; - OnUserBeganPlaying?.Invoke(userId, state); }); @@ -109,8 +105,8 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - playingUserStates.Remove(userId); - + if (watchingUsers.Contains(userId)) + playingUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); }); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index afbfb9f7ef..383f17d8d2 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private readonly IBindableDictionary playingUserStates = new BindableDictionary(); + private readonly IBindableDictionary userStates = new BindableDictionary(); private FillFlowContainer userFlow; @@ -51,33 +51,32 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - playingUserStates.BindTo(spectatorClient.PlayingUserStates); - playingUserStates.BindCollectionChanged(onUsersChanged, true); + userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindCollectionChanged(onUserStatesChanged, true); } - private void onUsersChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Replace: Debug.Assert(e.NewItems != null); - foreach ((int userId, _) in e.NewItems) + foreach ((int userId, SpectatorState state) in e.NewItems) { + if (state.State != SpectatingUserState.Playing) + { + removePlayingUser(userId); + continue; + } + users.GetUserAsync(userId).ContinueWith(task => { var user = task.GetResultSafely(); - if (user == null) return; - - Schedule(() => - { - // user may no longer be playing. - if (!playingUserStates.ContainsKey(user.Id)) - return; - - userFlow.Add(createUserPanel(user)); - }); + if (user != null) + Schedule(() => addPlayingUser(user)); }); } @@ -87,9 +86,20 @@ namespace osu.Game.Overlays.Dashboard Debug.Assert(e.OldItems != null); foreach ((int userId, _) in e.OldItems) - userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); + removePlayingUser(userId); break; } + + void addPlayingUser(APIUser user) + { + // user may no longer be playing. + if (!userStates.TryGetValue(user.Id, out var state2) || state2.State != SpectatingUserState.Playing) + return; + + userFlow.Add(createUserPanel(user)); + } + + void removePlayingUser(int userId) => userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); }); private PlayingUserPanel createUserPanel(APIUser user) => diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 3cf9f79611..783ba494eb 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Spectate [Resolved] private UserLookupCache userLookupCache { get; set; } - private readonly IBindableDictionary playingUserStates = new BindableDictionary(); + private readonly IBindableDictionary userStates = new BindableDictionary(); private readonly Dictionary userMap = new Dictionary(); private readonly Dictionary gameplayStates = new Dictionary(); @@ -77,8 +77,8 @@ namespace osu.Game.Screens.Spectate userMap[u.Id] = u; } - playingUserStates.BindTo(spectatorClient.PlayingUserStates); - playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); + userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindCollectionChanged(onUserStatesChanged, true); realmSubscription = realm.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); @@ -99,7 +99,7 @@ namespace osu.Game.Screens.Spectate { foreach ((int userId, _) in userMap) { - if (!playingUserStates.TryGetValue(userId, out var userState)) + if (!userStates.TryGetValue(userId, out var userState)) continue; if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID)) @@ -107,40 +107,41 @@ namespace osu.Game.Screens.Spectate } } - private void onPlayingUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) + private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) { switch (e.Action) { case NotifyDictionaryChangedAction.Add: foreach ((int userId, var state) in e.NewItems.AsNonNull()) - onUserStateAdded(userId, state); + onUserStateChanged(userId, state); break; case NotifyDictionaryChangedAction.Remove: - foreach ((int userId, var _) in e.OldItems.AsNonNull()) + foreach ((int userId, _) in e.OldItems.AsNonNull()) onUserStateRemoved(userId); break; case NotifyDictionaryChangedAction.Replace: - foreach ((int userId, var _) in e.OldItems.AsNonNull()) - onUserStateRemoved(userId); - foreach ((int userId, var state) in e.NewItems.AsNonNull()) - onUserStateAdded(userId, state); + onUserStateChanged(userId, state); break; } } - private void onUserStateAdded(int userId, SpectatorState state) + private void onUserStateChanged(int userId, SpectatorState newState) { - if (state.RulesetID == null || state.BeatmapID == null) + if (newState.RulesetID == null || newState.BeatmapID == null) return; if (!userMap.ContainsKey(userId)) return; - Schedule(() => OnUserStateChanged(userId, state)); - updateGameplayState(userId); + // Do nothing for failed/completed states. + if (newState.State == SpectatingUserState.Playing) + { + Schedule(() => OnUserStateChanged(userId, newState)); + updateGameplayState(userId); + } } private void onUserStateRemoved(int userId) @@ -162,7 +163,7 @@ namespace osu.Game.Screens.Spectate Debug.Assert(userMap.ContainsKey(userId)); var user = userMap[userId]; - var spectatorState = playingUserStates[userId]; + var spectatorState = userStates[userId]; var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.OnlineID == spectatorState.RulesetID)?.CreateInstance(); if (resolvedRuleset == null) From b75c08c9ab07232d44b1bde38f68f2054547b91c Mon Sep 17 00:00:00 2001 From: Spooghetti420 <83249561+Spooghetti420@users.noreply.github.com> Date: Tue, 1 Feb 2022 13:36:36 +0000 Subject: [PATCH 346/587] Improve beat length logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index 1abca970d5..6e942f672b 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Mania.Mods public static double GetNoteDurationInBeatLength(HoldNote holdNote, ManiaBeatmap beatmap) { - double bpmAtNoteTime = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BPM; - return (60 * holdNote.Duration) / (1000 * bpmAtNoteTime); + double beatLength = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BeatLength; + return holdNote.Duration / beatLength; } } } From 39524f3dd296d44cfb8187507a416e02a47b040c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 1 Feb 2022 22:26:30 +0800 Subject: [PATCH 347/587] Split total pp into 2 lines --- .../Statistics/PerformanceStatisticTooltip.cs | 85 ++++++++++++++++--- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 44e5c366bb..739fac9d63 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -72,16 +72,73 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics UpdateDisplay(performance); } - private Drawable createAttributeItem(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) + private Drawable createItemForTotal(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) { - bool isTotal = attribute.PropertyName == nameof(PerformanceAttributes.Total); - float fraction = (float)(attribute.Value / perfectAttribute.Value); - if (float.IsNaN(fraction)) - fraction = 0; - string text = fraction.ToLocalisableString("0%").ToString(); + return + new GridContainer + { + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Bottom = 10 }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 250), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = attribute.DisplayName, + Colour = titleColor + }, + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Text = Math.Round(attribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(), + Colour = titleColor + } + }, + new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = "Maximum", + Colour = OsuColour.Gray(0.7f) + }, + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(), + Colour = OsuColour.Gray(0.7f) + } + } + } + }; + } - if (isTotal) - text = (int)Math.Round(attribute.Value, MidpointRounding.AwayFromZero) + "/" + (int)Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero); + private Drawable createItemForAttribute(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) + { + float percentage = (float)(attribute.Value / perfectAttribute.Value); + if (float.IsNaN(percentage)) + percentage = 0; + string text = percentage.ToLocalisableString("0%").ToString(); return new GridContainer { @@ -106,7 +163,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.Regular), Text = attribute.DisplayName, - Colour = isTotal ? titleColor : textColour + Colour = textColour }, new Bar { @@ -115,8 +172,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Width = 130, Height = 5, BackgroundColour = Color4.White.Opacity(0.5f), - Colour = isTotal ? titleColor : textColour, - Length = fraction, + Colour = textColour, + Length = percentage, Margin = new MarginPadding { Left = 5, Right = 5 } }, new OsuSpriteText @@ -125,7 +182,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.SemiBold), Text = text, - Colour = isTotal ? titleColor : textColour + Colour = textColour } } } @@ -142,7 +199,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics foreach (PerformanceDisplayAttribute attr in displayAttributes) { - Content.Add(createAttributeItem(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName))); + Content.Add(attr.PropertyName == nameof(PerformanceAttributes.Total) + ? createItemForTotal(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)) + : createItemForAttribute(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName))); } } From b06128ffa5f4845bd0c9ae6dabe11120336b5cf4 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 1 Feb 2022 22:26:55 +0800 Subject: [PATCH 348/587] Rename "Final PP" to "Achieved PP" --- osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index f210c669a6..e8c4c71913 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Difficulty /// public virtual IEnumerable GetAttributesForDisplay() { - yield return new PerformanceDisplayAttribute(nameof(Total), "Final PP", Total); + yield return new PerformanceDisplayAttribute(nameof(Total), "Achieved PP", Total); } } } From eee020f8e4a17db7a1af6be611cbc0b3c5554852 Mon Sep 17 00:00:00 2001 From: dekrain Date: Tue, 1 Feb 2022 20:26:52 +0100 Subject: [PATCH 349/587] Cleanup tooltip layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Leaderboards/LeaderboardScoreTooltip.cs | 42 ++++++------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index 8c349d56e0..a0eea94501 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -10,7 +10,6 @@ using osuTK; using osu.Game.Graphics.Sprites; using osu.Game.Graphics; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -49,7 +48,7 @@ namespace osu.Game.Online.Leaderboards }, new FillFlowContainer { - Margin = new MarginPadding(5f), + Margin = new MarginPadding(5), Spacing = new Vector2(10), AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, @@ -63,8 +62,10 @@ namespace osu.Game.Online.Leaderboards // Mods row modStatistics = new FillFlowContainer { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Full, + Spacing = new Vector2(5, 0), }, new FillFlowContainer { @@ -77,11 +78,13 @@ namespace osu.Game.Online.Leaderboards { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), }, bottomScoreStatistics = new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), }, } }, @@ -133,7 +136,6 @@ namespace osu.Game.Online.Leaderboards public HitResultCell(HitResultDisplayStatistic stat) { AutoSizeAxes = Axes.Both; - Padding = new MarginPadding { Horizontal = 5f }; displayName = stat.DisplayName; result = stat.Result; @@ -148,35 +150,17 @@ namespace osu.Game.Online.Leaderboards Height = 12, AutoSizeAxes = Axes.X, Direction = FillDirection.Horizontal, - Spacing = new Vector2(2f, 0f), + Spacing = new Vector2(5f, 0f), Children = new Drawable[] { - new CircularContainer + new OsuSpriteText { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#222") - }, - new OsuSpriteText - { - Padding = new MarginPadding(2), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), - Text = displayName.ToUpperInvariant(), - Colour = colours.ForHitResult(result), - } - } + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + Text = displayName.ToUpperInvariant(), + Colour = colours.ForHitResult(result), }, new OsuSpriteText { - RelativeSizeAxes = Axes.Y, Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Text = count.ToString(), }, @@ -192,7 +176,6 @@ namespace osu.Game.Online.Leaderboards public ModCell(Mod mod) { AutoSizeAxes = Axes.Both; - Padding = new MarginPadding { Horizontal = 5f }; this.mod = mod; } @@ -228,6 +211,7 @@ namespace osu.Game.Online.Leaderboards Text = mod.SettingDescription, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Top = 1 }, }); } } From 3d7af805a3cd0905fef3bfc15544330d3bdcd912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:16:28 +0100 Subject: [PATCH 350/587] Fix `BeatmapMetadata` not using its user param correctly --- osu.Game/Beatmaps/BeatmapMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 1514d3af7a..f6666a6ea9 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -46,7 +46,7 @@ namespace osu.Game.Beatmaps public BeatmapMetadata(RealmUser? user = null) { - Author = new RealmUser(); + Author = user ?? new RealmUser(); } [UsedImplicitly] // Realm From a378e78ced3e0b5dfe6dafa3803060b1c9d118ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:17:27 +0100 Subject: [PATCH 351/587] Fix `RealmLive` unnecessarily passing ID around Appears to have never been needed. When the `retrieveFromID` method was created in 81b5717ae793e334fdcf8efd72d20debfb49e9ca, it didn't use the `id` parameter for anything either. --- osu.Game/Database/RealmLive.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 186e801425..ecfececaa4 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -63,7 +63,7 @@ namespace osu.Game.Database return; } - perform(retrieveFromID(r, ID)); + perform(retrieveFromID(r)); RealmLiveStatistics.USAGE_ASYNC.Value++; }); } @@ -85,7 +85,7 @@ namespace osu.Game.Database return realm.Run(r => { - var returnData = perform(retrieveFromID(r, ID)); + var returnData = perform(retrieveFromID(r)); RealmLiveStatistics.USAGE_ASYNC.Value++; if (returnData is RealmObjectBase realmObject && realmObject.IsManaged) @@ -139,11 +139,11 @@ namespace osu.Game.Database } dataIsFromUpdateThread = true; - data = retrieveFromID(realm.Realm, ID); + data = retrieveFromID(realm.Realm); RealmLiveStatistics.USAGE_UPDATE_REFETCH.Value++; } - private T retrieveFromID(Realm realm, Guid id) + private T retrieveFromID(Realm realm) { var found = realm.Find(ID); From 16e0cc6a2b393312f90da6320d6d9e4d00685e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:20:59 +0100 Subject: [PATCH 352/587] Remove `IIpcHost` param from `ScoreManager` No longer used since 3e3b9bc963583fa5f6ad3b822ad3e6e3818aa06c. --- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8363c41437..1713e73905 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -247,7 +247,7 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() - dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, Host, () => difficultyCache, LocalConfig)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, () => difficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API)); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 6f9cce2d3c..532c6b42a3 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -32,7 +32,7 @@ namespace osu.Game.Scoring private readonly ScoreModelManager scoreModelManager; public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler, - IIpcHost importHost = null, Func difficulties = null, OsuConfigManager configManager = null) + Func difficulties = null, OsuConfigManager configManager = null) { this.realm = realm; this.scheduler = scheduler; From c6a65ccfeddc3dd190d38b0629fbb9efbd64fab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:22:22 +0100 Subject: [PATCH 353/587] Remove unused parameter from `createContent()` No longer used since 513e470b525c4948d793555beb0df0928b3b9019. --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 1e6899e05f..0c12eff503 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Timing } Columns = createHeaders(); - Content = value.Select((g, i) => createContent(i, g)).ToArray().ToRectangular(); + Content = value.Select(createContent).ToArray().ToRectangular(); } } @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit.Timing return columns.ToArray(); } - private Drawable[] createContent(int index, ControlPointGroup group) + private Drawable[] createContent(ControlPointGroup group) { return new Drawable[] { From 1fa2bf5d69f0c92755f9191acb4ae686b160fa9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:27:14 +0100 Subject: [PATCH 354/587] Remove unused parameter from `createColourBars()` No longer used since b61aa660c63f0e126f32643ef17e7bb458d2a1ae. --- .../Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index a8141c57da..7903e54960 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osuTK; @@ -49,7 +48,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { InternalChild = new FillFlowContainer { @@ -127,7 +126,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters } }; - createColourBars(colours); + createColourBars(); } protected override void LoadComplete() @@ -150,7 +149,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters iconLate.Rotation = -Rotation; } - private void createColourBars(OsuColour colours) + private void createColourBars() { var windows = HitWindows.GetAllAvailableWindows().ToArray(); From a94702b3aeaac4b1dfe1e0adeb71140650bcaabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:30:28 +0100 Subject: [PATCH 355/587] Remove unused parameters in `LegacyComboCounter` No longer used since 9bb8a43bcee61d11f26598fff2228af1f07a4d17. --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 567e4386c6..9510453ba5 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play.HUD return; if (isRolling) - onDisplayedCountRolling(displayedCount, value); + onDisplayedCountRolling(value); else if (displayedCount + 1 == value) onDisplayedCountIncrement(value); else @@ -151,7 +151,7 @@ namespace osu.Game.Screens.Play.HUD if (prev + 1 == Current.Value) onCountIncrement(prev, Current.Value); else - onCountChange(prev, Current.Value); + onCountChange(Current.Value); } else { @@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.HUD transformRoll(currentValue, newValue); } - private void onCountChange(int currentValue, int newValue) + private void onCountChange(int newValue) { scheduledPopOutCurrentId++; @@ -236,7 +236,7 @@ namespace osu.Game.Screens.Play.HUD DisplayedCount = newValue; } - private void onDisplayedCountRolling(int currentValue, int newValue) + private void onDisplayedCountRolling(int newValue) { if (newValue == 0) displayedCountSpriteText.FadeOut(fade_out_duration); From 07d09b3520210f1233cb024e8419548f342c4001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:32:58 +0100 Subject: [PATCH 356/587] Remove unused parameter from `createGameplayComponents()` No longer used since 136843c8e450507ad0527622af851d648afb1545. --- osu.Game/Screens/Play/Player.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 1556e752ab..240620b686 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -251,7 +251,7 @@ namespace osu.Game.Screens.Play { // underlay and gameplay should have access to the skinning sources. createUnderlayComponents(), - createGameplayComponents(Beatmap.Value, playableBeatmap) + createGameplayComponents(Beatmap.Value) } }, FailOverlay = new FailOverlay @@ -364,7 +364,7 @@ namespace osu.Game.Screens.Play private Drawable createUnderlayComponents() => DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; - private Drawable createGameplayComponents(IWorkingBeatmap working, IBeatmap playableBeatmap) => new ScalingContainer(ScalingMode.Gameplay) + private Drawable createGameplayComponents(IWorkingBeatmap working) => new ScalingContainer(ScalingMode.Gameplay) { Children = new Drawable[] { From 994fb966b6ba27baf7c178a7483ccf353db46fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:37:19 +0100 Subject: [PATCH 357/587] Remove `Host` ctor param from `SkinModelManager` No longer used since 29d074bdb8629ff480207498c0eeec61b010a530. --- osu.Game/Skinning/SkinManager.cs | 2 +- osu.Game/Skinning/SkinModelManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 06bd0abc9f..bad559d9fe 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -77,7 +77,7 @@ namespace osu.Game.Skinning userFiles = new StorageBackedResourceStore(storage.GetStorageForDirectory("files")); - skinModelManager = new SkinModelManager(storage, realm, host, this); + skinModelManager = new SkinModelManager(storage, realm, this); var defaultSkins = new[] { diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index 0af31100a9..33e49ce486 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -27,7 +27,7 @@ namespace osu.Game.Skinning private readonly IStorageResourceProvider skinResources; - public SkinModelManager(Storage storage, RealmAccess realm, GameHost host, IStorageResourceProvider skinResources) + public SkinModelManager(Storage storage, RealmAccess realm, IStorageResourceProvider skinResources) : base(storage, realm) { this.skinResources = skinResources; From 75101b11052c2f02471a8fed6129f9d0e4d5eb76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:39:52 +0100 Subject: [PATCH 358/587] Remove unused ruleset ctor params from test beatmap model managers No longer used since 00e3af3366e80214c7f0c956d144000a78cd0b56. --- .../Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs | 4 ++-- osu.Game/Tests/Visual/EditorTestScene.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index f9161816e7..79cb424803 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -173,14 +173,14 @@ namespace osu.Game.Tests.Online protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) { - return new TestBeatmapModelManager(this, storage, realm, rulesets, onlineLookupQueue); + return new TestBeatmapModelManager(this, storage, realm, onlineLookupQueue); } internal class TestBeatmapModelManager : BeatmapModelManager { private readonly TestBeatmapManager testBeatmapManager; - public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) + public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) : base(databaseAccess, storage, beatmapOnlineLookupQueue) { this.testBeatmapManager = testBeatmapManager; diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index f7d62a8694..bcf169bb1e 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) { - return new TestBeatmapModelManager(storage, realm, rulesets, onlineLookupQueue); + return new TestBeatmapModelManager(storage, realm, onlineLookupQueue); } protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) @@ -150,7 +150,7 @@ namespace osu.Game.Tests.Visual internal class TestBeatmapModelManager : BeatmapModelManager { - public TestBeatmapModelManager(Storage storage, RealmAccess databaseAccess, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) + public TestBeatmapModelManager(Storage storage, RealmAccess databaseAccess, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) : base(databaseAccess, storage, beatmapOnlineLookupQueue) { } From e042f29ee3a12e60eb6d06165db93af1c37faf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:41:18 +0100 Subject: [PATCH 359/587] Remove skin ctor param from `LegacyCatchComboCounter` No longer used since 004798d61d44d88114acfd7c2861378261eab107. --- .../Skinning/Legacy/CatchLegacySkinTransformer.cs | 2 +- .../Skinning/Legacy/LegacyCatchComboCounter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index faad95e386..b2a555f89d 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy case CatchSkinComponents.CatchComboCounter: if (providesComboCounter) - return new LegacyCatchComboCounter(Skin); + return new LegacyCatchComboCounter(); return null; diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs index 33c3867f5a..b4d29988d9 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy private readonly LegacyRollingCounter explosion; - public LegacyCatchComboCounter(ISkin skin) + public LegacyCatchComboCounter() { AutoSizeAxes = Axes.Both; From e4028b8fc1b86df31a6882480797cf0094c0a027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:42:58 +0100 Subject: [PATCH 360/587] Remove index ctor param from `ColumnHitObjectArea` No longer used since 5692cecaa47e9e071fb6079d9f3847b8b1de0972. --- .../Skinning/TestSceneColumnHitObjectArea.cs | 4 ++-- osu.Game.Rulesets.Mania/UI/Column.cs | 2 +- osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs index 215f8fb1d5..8034341d15 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new ColumnHitObjectArea(0, new HitObjectContainer()) + Child = new ColumnHitObjectArea(new HitObjectContainer()) { RelativeSizeAxes = Axes.Both } @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new ColumnHitObjectArea(1, new HitObjectContainer()) + Child = new ColumnHitObjectArea(new HitObjectContainer()) { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index dc3b86ac7f..a04f5ef98e 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.UI sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer), // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements background.CreateProxy(), - HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both }, + HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both }, new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) { RelativeSizeAxes = Axes.Both diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index f69d2aafdc..51c138f5e1 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components private readonly Drawable hitTarget; - public ColumnHitObjectArea(int columnIndex, HitObjectContainer hitObjectContainer) + public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) : base(hitObjectContainer) { AddRangeInternal(new[] From 7cdf63c6543034a7517ec17949874d16bdd1b47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:44:59 +0100 Subject: [PATCH 361/587] Remove unused `FindProvider()` methods No longer needed since 39f99bf785676c443f37248668eb9bf45006032e. --- osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs | 1 - osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs | 2 -- osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs | 1 - osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs | 4 ---- 4 files changed, 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index 1f01ba601b..a36f07ff7b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -118,7 +118,6 @@ namespace osu.Game.Rulesets.Osu.Tests public Drawable GetDrawableComponent(ISkinComponent component) => null; public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public ISample GetSample(ISampleInfo sampleInfo) => null; - public ISkin FindProvider(Func lookupFunction) => null; public IBindable GetConfig(TLookup lookup) { diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index 34f70659e3..5553c67141 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -131,8 +131,6 @@ namespace osu.Game.Tests.Gameplay public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); - public ISkin FindProvider(Func lookupFunction) => null; - public IBindable GetConfig(TLookup lookup) { switch (lookup) diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs index 785f31386d..4209f188cc 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs @@ -61,7 +61,6 @@ namespace osu.Game.Tests.NonVisual.Skinning public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException(); public ISample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException(); public IBindable GetConfig(TLookup lookup) => throw new NotSupportedException(); - public ISkin FindProvider(Func lookupFunction) => null; } private class TestAnimationTimeReference : IAnimationTimeReference diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index 3e8ba69e01..35130f3109 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -301,8 +301,6 @@ namespace osu.Game.Tests.Visual.Gameplay public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException(); - - public ISkin FindProvider(Func lookupFunction) => throw new NotImplementedException(); } private class SecondarySource : ISkin @@ -314,8 +312,6 @@ namespace osu.Game.Tests.Visual.Gameplay public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException(); - - public ISkin FindProvider(Func lookupFunction) => throw new NotImplementedException(); } [Cached(typeof(ISkinSource))] From b978010b481fd8e46a7b0448b813a3e547d375ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:47:55 +0100 Subject: [PATCH 362/587] Remove unused `allowMissing` parameter in audio file check test No longer used since 7f95400f466beafe6fa94c002cfd6ed8dc3f4f94. --- .../Editing/Checks/CheckTooShortAudioFilesTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs index 242fec2f68..53c85defae 100644 --- a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Editing.Checks beatmap.BeatmapInfo.BeatmapSet.Files.Add(CheckTestHelpers.CreateMockFile("jpg")); // Should fail to load, but not produce an error due to the extension not being expected to load. - Assert.IsEmpty(check.Run(getContext(null, allowMissing: true))); + Assert.IsEmpty(check.Run(getContext(null))); } [Test] @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Editing.Checks { using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3")) { - Assert.IsEmpty(check.Run(getContext(resourceStream, allowMissing: true))); + Assert.IsEmpty(check.Run(getContext(resourceStream))); } } @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Editing.Checks } } - private BeatmapVerifierContext getContext(Stream resourceStream, bool allowMissing = false) + private BeatmapVerifierContext getContext(Stream resourceStream) { var mockWorkingBeatmap = new Mock(beatmap, null, null); mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); From 3674ed15ce7cc4e5b0e50303e04db4af6eba3164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Feb 2022 21:52:16 +0100 Subject: [PATCH 363/587] Remove unused game host parameter No longer used since eeccf836ec04d2e3aac1fec93c15b48aa6672352. --- .../Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 79cb424803..94e61eaee0 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Online { Dependencies.Cache(rulesets = new RulesetStore(Realm)); Dependencies.CacheAs(beatmaps = new TestBeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); - Dependencies.CacheAs(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API, host)); + Dependencies.CacheAs(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API)); } [SetUp] @@ -196,7 +196,7 @@ namespace osu.Game.Tests.Online internal class TestBeatmapModelDownloader : BeatmapModelDownloader { - public TestBeatmapModelDownloader(IModelImporter importer, IAPIProvider apiProvider, GameHost gameHost) + public TestBeatmapModelDownloader(IModelImporter importer, IAPIProvider apiProvider) : base(importer, apiProvider) { } From 35b76532903254b3ea434f7fccf9ef95d07be72c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 09:13:19 +0900 Subject: [PATCH 364/587] Revert mod flow changes and add visual test coverage showing an overflow case --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 12 +++++++----- .../Online/Leaderboards/LeaderboardScoreTooltip.cs | 5 ++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 7292ec96ed..667fd08084 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -203,14 +203,16 @@ namespace osu.Game.Tests.Visual.SongSelect new OsuModHardRock(), new OsuModFlashlight { - FollowDelay = { Value = 200 } + FollowDelay = { Value = 200 }, + SizeMultiplier = { Value = 5 }, }, new OsuModDifficultyAdjust { - CircleSize = { Value = 8 }, - ApproachRate = { Value = 7 }, - OverallDifficulty = { Value = 6 }, - DrainRate = { Value = 5 }, + CircleSize = { Value = 11 }, + ApproachRate = { Value = 10 }, + OverallDifficulty = { Value = 10 }, + DrainRate = { Value = 10 }, + ExtendedLimits = { Value = true } } }, Ruleset = new OsuRuleset().RulesetInfo, diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index a0eea94501..c26e9e6802 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -62,9 +62,8 @@ namespace osu.Game.Online.Leaderboards // Mods row modStatistics = new FillFlowContainer { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Full, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, Spacing = new Vector2(5, 0), }, new FillFlowContainer From b4fd1ecba20a10858a1219beddabf8122517fa22 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 11:02:01 +0800 Subject: [PATCH 365/587] Hide attribute if the maximum is 0 --- .../Statistics/PerformanceStatisticTooltip.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs index 739fac9d63..bd6eb057c6 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs @@ -137,8 +137,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { float percentage = (float)(attribute.Value / perfectAttribute.Value); if (float.IsNaN(percentage)) - percentage = 0; - string text = percentage.ToLocalisableString("0%").ToString(); + return null; return new GridContainer { @@ -181,7 +180,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = text, + Text = percentage.ToLocalisableString("0%"), Colour = textColour } } @@ -199,9 +198,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics foreach (PerformanceDisplayAttribute attr in displayAttributes) { - Content.Add(attr.PropertyName == nameof(PerformanceAttributes.Total) + var attributeItem = attr.PropertyName == nameof(PerformanceAttributes.Total) ? createItemForTotal(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)) - : createItemForAttribute(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName))); + : createItemForAttribute(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)); + + if (attributeItem != null) + Content.Add(attributeItem); } } From d065e32ca1f8c77b361593016bedf0487540fcff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 13:23:49 +0900 Subject: [PATCH 366/587] Fix crash due to `MatchLeaderboardScore`s not having populated rulesets --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- .../OnlinePlay/Match/Components/MatchLeaderboardScore.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index b4aaebd4e4..c2393a5de5 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -71,7 +71,7 @@ namespace osu.Game.Online.Leaderboards private Storage storage { get; set; } public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); - public ScoreInfo TooltipContent => Score; + public virtual ScoreInfo TooltipContent => Score; public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true) { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs index 799c44cc28..cf7e33fd63 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs @@ -14,6 +14,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components { private readonly APIUserScoreAggregate score; + public override ScoreInfo TooltipContent => null; // match aggregate scores can't show statistics that the custom tooltip displays. + public MatchLeaderboardScore(APIUserScoreAggregate score, int? rank, bool isOnlineScope = true) : base(score.CreateScoreInfo(), rank, isOnlineScope) { From ddc8094a75babd5fad94ea8b692b8e12a899c461 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 13:34:23 +0900 Subject: [PATCH 367/587] Update description --- osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index 6e942f672b..a65938184c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; - public override string Description => @"Turns all hold notes into normal notes. No coordination required."; + public override string Description => @"Replaces all hold notes with normal notes."; public override IconUsage? Icon => FontAwesome.Solid.DotCircle; From 0036d0e26db709f1661e3cff89364ccb7c6f94b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 13:58:13 +0900 Subject: [PATCH 368/587] Move alternate mod to "conversion" category --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index f68f2b5bee..936bb290b4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => @"Don't use the same key twice in a row!"; public override double ScoreMultiplier => 1.0; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) }; - public override ModType Type => ModType.DifficultyIncrease; + public override ModType Type => ModType.Conversion; public override IconUsage? Icon => FontAwesome.Solid.Keyboard; private bool introEnded; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 7e8974b5ed..2faecbcda2 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -159,7 +159,6 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()), new OsuModHidden(), new MultiMod(new OsuModFlashlight(), new OsuModBlinds()), - new OsuModAlternate(), }; case ModType.Conversion: @@ -170,6 +169,7 @@ namespace osu.Game.Rulesets.Osu new OsuModClassic(), new OsuModRandom(), new OsuModMirror(), + new OsuModAlternate(), }; case ModType.Automation: From fed63abd83715cf1f904a53038999b7625e41f4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 14:02:48 +0900 Subject: [PATCH 369/587] Sanitise interceptor logic to now require two separate check paths --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index 936bb290b4..9d389432e5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -33,8 +33,6 @@ namespace osu.Game.Rulesets.Osu.Mods private OsuAction? lastActionPressed; private DrawableRuleset ruleset; - private bool shouldAlternate => !isBreakTime.Value && introEnded; - public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { ruleset = drawableRuleset; @@ -54,16 +52,22 @@ namespace osu.Game.Rulesets.Osu.Mods }; } - private bool onPressed(OsuAction key) + private bool checkCorrectAction(OsuAction action) { - if (lastActionPressed == key) + if (isBreakTime.Value) + return true; + + if (!introEnded) + return true; + + if (lastActionPressed != action) { - ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint); + // User alternated correctly + lastActionPressed = action; return true; } - lastActionPressed = key; - + ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint); return false; } @@ -83,7 +87,8 @@ namespace osu.Game.Rulesets.Osu.Mods } public bool OnPressed(KeyBindingPressEvent e) - => mod.shouldAlternate && mod.onPressed(e.Action); + // if the pressed action is incorrect, block it from reaching gameplay. + => !mod.checkCorrectAction(e.Action); public void OnReleased(KeyBindingReleaseEvent e) { From c7a192cc5fae75bca23b5a714d6ff2fde49b6bbf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 14:04:10 +0900 Subject: [PATCH 370/587] Only handle `LeftButton` and `RightButton` actions There are definitely going to be other actions used in the future, which would immediately cause this mod to fail. Limiting handling to left/right buttons only is the correct way forward. --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index 9d389432e5..b9f25bd1cf 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -60,9 +60,20 @@ namespace osu.Game.Rulesets.Osu.Mods if (!introEnded) return true; + switch (action) + { + case OsuAction.LeftButton: + case OsuAction.RightButton: + break; + + // Any action which is not left or right button should be ignored. + default: + return true; + } + if (lastActionPressed != action) { - // User alternated correctly + // User alternated correctly. lastActionPressed = action; return true; } From c5c4c85006ffd4a34e05ba33d6e81bb9acf8acc8 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 13:29:18 +0800 Subject: [PATCH 371/587] Lazily create content of StatisticItem --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 22 +++++++++------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 25 +++++++++++-------- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 22 +++++++++------- .../Ranking/Statistics/StatisticContainer.cs | 2 +- .../Ranking/Statistics/StatisticItem.cs | 12 +++++++-- .../Ranking/Statistics/StatisticsPanel.cs | 10 +++++--- 6 files changed, 59 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2098c7f5d8..8ac8001457 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -370,21 +370,25 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", + true, + () => new HitEventTimingDistributionGraph(score.HitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(score.HitEvents) - })) + new StatisticItem(string.Empty, + true, + () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(score.HitEvents) + })) } } }; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 18e4bb259c..b6f417b7fe 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -278,7 +278,8 @@ namespace osu.Game.Rulesets.Osu Columns = new[] { new StatisticItem("Timing Distribution", - new HitEventTimingDistributionGraph(timedHitEvents) + true, + () => new HitEventTimingDistributionGraph(timedHitEvents) { RelativeSizeAxes = Axes.X, Height = 250 @@ -289,21 +290,25 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Accuracy Heatmap", + true, + () => new AccuracyHeatmap(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(timedHitEvents) - })) + new StatisticItem(string.Empty, + true, + () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) } } }; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index ca860f24c3..2a64b8dddd 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -213,21 +213,25 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", + true, + () => new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(timedHitEvents) - })) + new StatisticItem(string.Empty, + true, + () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) } } }; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index 485d24d024..f3bd2c6fc1 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 15 }, - Child = item.Content + Child = item.Content() } }, }, diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index 4903983759..5e6ddf445b 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.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 JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -20,22 +21,29 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The content to be displayed. /// - public readonly Drawable Content; + public readonly Func Content; /// /// The of this row. This can be thought of as the column dimension of an encompassing . /// public readonly Dimension Dimension; + /// + /// Whether this item requires hit events. If true, the will not be created if no hit events are available. + /// + public readonly bool RequiresHitEvents; + /// /// Creates a new , to be displayed inside a in the results screen. /// /// The name of the item. Can be to hide the item header. + /// Whether this item requires hit events. If true, the will not be created if no hit events are available. /// The content to be displayed. /// The of this item. This can be thought of as the column dimension of an encompassing . - public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) + public StatisticItem([NotNull] string name, bool requiresHitEvents, [NotNull] Func content, [CanBeNull] Dimension dimension = null) { Name = name; + RequiresHitEvents = requiresHitEvents; Content = content; Dimension = dimension; } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 567a2307dd..14cdfe2f03 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -118,6 +118,10 @@ namespace osu.Game.Screens.Ranking.Statistics foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) { + var columnsToDisplay = newScore.HitEvents.Count == 0 + ? row.Columns?.Where(c => !c.RequiresHitEvents).ToArray() + : row.Columns; + rows.Add(new GridContainer { Anchor = Anchor.TopCentre, @@ -126,14 +130,14 @@ namespace osu.Game.Screens.Ranking.Statistics AutoSizeAxes = Axes.Y, Content = new[] { - row.Columns?.Select(c => new StatisticContainer(c) + columnsToDisplay?.Select(c => new StatisticContainer(c) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }).Cast().ToArray() }, - ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) - .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), + ColumnDimensions = Enumerable.Range(0, columnsToDisplay?.Length ?? 0) + .Select(i => columnsToDisplay?[i].Dimension ?? new Dimension()).ToArray(), RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } From a2affefb0aa8ca794ff329a1a7e65e3502450f35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 14:33:17 +0900 Subject: [PATCH 372/587] Avoid checking gameplay clock time in `Update` method --- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index b9f25bd1cf..46b97dd23b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -16,7 +16,7 @@ using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModAlternate : Mod, IApplicableToDrawableRuleset, IApplicableToPlayer, IUpdatableByPlayfield + public class OsuModAlternate : Mod, IApplicableToDrawableRuleset, IApplicableToPlayer { public override string Name => @"Alternate"; public override string Acronym => @"AL"; @@ -26,20 +26,23 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Conversion; public override IconUsage? Icon => FontAwesome.Solid.Keyboard; - private bool introEnded; - private double earliestStartTime; + private double firstObjectValidJudgementTime; private IBindable isBreakTime; private const double flash_duration = 1000; private OsuAction? lastActionPressed; private DrawableRuleset ruleset; + private IFrameStableClock gameplayClock; + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { ruleset = drawableRuleset; drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this)); var firstHitObject = ruleset.Objects.FirstOrDefault(); - earliestStartTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0); + firstObjectValidJudgementTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0); + + gameplayClock = drawableRuleset.FrameStableClock; } public void ApplyToPlayer(Player player) @@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (isBreakTime.Value) return true; - if (!introEnded) + if (gameplayClock.CurrentTime < firstObjectValidJudgementTime) return true; switch (action) @@ -82,12 +85,6 @@ namespace osu.Game.Rulesets.Osu.Mods return false; } - public void Update(Playfield playfield) - { - if (!introEnded) - introEnded = playfield.Clock.CurrentTime > earliestStartTime; - } - private class InputInterceptor : Component, IKeyBindingHandler { private readonly OsuModAlternate mod; From 3ba5d88914d09e1725ee3abaec9fd44699e3973c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 13:41:51 +0800 Subject: [PATCH 373/587] Update statistics item display logic --- .../Ranking/Statistics/StatisticsPanel.cs | 131 +++++++++--------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 14cdfe2f03..9d89aa213c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -74,85 +74,92 @@ namespace osu.Game.Screens.Ranking.Statistics if (newScore == null) return; - if (newScore.HitEvents.Count == 0) + spinner.Show(); + + var localCancellationSource = loadCancellation = new CancellationTokenSource(); + IBeatmap playableBeatmap = null; + + // 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(() => { - content.Add(new FillFlowContainer + playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); + }, loadCancellation.Token).ContinueWith(t => Schedule(() => + { + var rows = new FillFlowContainer { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Direction = FillDirection.Vertical, - Children = new Drawable[] + Spacing = new Vector2(30, 15), + Alpha = 0 + }; + + bool panelIsEmpty = true; + bool hitEventsAvailable = newScore.HitEvents.Count != 0; + + foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) + { + var columnsToDisplay = hitEventsAvailable + ? row.Columns + : row.Columns?.Where(c => !c.RequiresHitEvents).ToArray(); + + if (columnsToDisplay?.Any() ?? false) + panelIsEmpty = false; + + rows.Add(new GridContainer { - new MessagePlaceholder("Extended statistics are only available after watching a replay!"), - new ReplayDownloadButton(newScore) + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { - Scale = new Vector2(1.5f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + columnsToDisplay?.Select(c => new StatisticContainer(c) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }).Cast().ToArray() }, - } - }); - } - else - { - spinner.Show(); + ColumnDimensions = Enumerable.Range(0, columnsToDisplay?.Length ?? 0) + .Select(i => columnsToDisplay?[i].Dimension ?? new Dimension()).ToArray(), + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }); + } - var localCancellationSource = loadCancellation = new CancellationTokenSource(); - IBeatmap playableBeatmap = null; - - // 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(() => + if (!hitEventsAvailable) { - playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); - }, loadCancellation.Token).ContinueWith(t => Schedule(() => - { - var rows = new FillFlowContainer + rows.Add(new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, Direction = FillDirection.Vertical, - Spacing = new Vector2(30, 15), - Alpha = 0 - }; - - foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) - { - var columnsToDisplay = newScore.HitEvents.Count == 0 - ? row.Columns?.Where(c => !c.RequiresHitEvents).ToArray() - : row.Columns; - - rows.Add(new GridContainer + Children = new Drawable[] { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] + new MessagePlaceholder(panelIsEmpty + ? "Extended statistics are only available after watching a replay!" + : "More statistics available after watching a replay!"), + new ReplayDownloadButton(newScore) { - columnsToDisplay?.Select(c => new StatisticContainer(c) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }).Cast().ToArray() + Scale = new Vector2(1.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }, - ColumnDimensions = Enumerable.Range(0, columnsToDisplay?.Length ?? 0) - .Select(i => columnsToDisplay?[i].Dimension ?? new Dimension()).ToArray(), - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }); - } + } + }); + } - LoadComponentAsync(rows, d => - { - if (!Score.Value.Equals(newScore)) - return; + LoadComponentAsync(rows, d => + { + if (!Score.Value.Equals(newScore)) + return; - spinner.Hide(); - content.Add(d); - d.FadeIn(250, Easing.OutQuint); - }, localCancellationSource.Token); - }), localCancellationSource.Token); - } + spinner.Hide(); + content.Add(d); + d.FadeIn(250, Easing.OutQuint); + }, localCancellationSource.Token); + }), localCancellationSource.Token); } protected override bool OnClick(ClickEvent e) From 6e60e68b809be7e1d93f3a80371f1ba551d45c43 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 2 Feb 2022 14:44:06 +0900 Subject: [PATCH 374/587] Change from click to mousedown+mouseup and only play when cursor is visible --- osu.Game/Graphics/Cursor/MenuCursor.cs | 29 +++++++++++++++----------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index a89d8dac71..0cc751ea21 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -76,18 +76,6 @@ namespace osu.Game.Graphics.Cursor return base.OnMouseMove(e); } - protected override bool OnClick(ClickEvent e) - { - var channel = tapSample.GetChannel(); - - // scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird) - channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75; - channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02); - channel.Play(); - - return base.OnClick(e); - } - protected override bool OnMouseDown(MouseDownEvent e) { if (State.Value == Visibility.Visible) @@ -105,6 +93,8 @@ namespace osu.Game.Graphics.Cursor dragRotationState = DragRotationState.DragStarted; positionMouseDown = e.MousePosition; } + + playTapSample(); } return base.OnMouseDown(e); @@ -122,6 +112,9 @@ namespace osu.Game.Graphics.Cursor activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf); dragRotationState = DragRotationState.NotDragging; } + + if (State.Value == Visibility.Visible) + playTapSample(0.8); } base.OnMouseUp(e); @@ -139,6 +132,18 @@ namespace osu.Game.Graphics.Cursor activeCursor.ScaleTo(0.6f, 250, Easing.In); } + private void playTapSample(double baseFrequency = 1f) + { + const float random_range = 0.02f; + SampleChannel channel = tapSample.GetChannel(); + + // Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird) + channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75; + channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range); + + channel.Play(); + } + public class Cursor : Container { private Container cursorContainer; From 0c5da9370a239b957f6d7a2fce929c0b7cff323c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 14:51:55 +0900 Subject: [PATCH 375/587] Fix rulesets potentially being marked `Available` even when methods are missing Came up when running the game after the recent breaking changes (https://github.com/ppy/osu/pull/16722), where two template rulesets I had loaded were erroring on startup but still being marked as available, allowing them to crash the game on attempting to initiate relpay logic. These cases are already handled for first-time ruleset loading via the `GetTypes()` enumeration in `RulesetStore.addRuleset`, but when consistency checking already present rulesets the only runtime validation being done was `ruleset.CreateInstance()`, which does not handle missing types or methods. --- osu.Game/Rulesets/RulesetStore.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index d017d54ed9..dd25005006 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -149,6 +149,10 @@ namespace osu.Game.Rulesets var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo ?? throw new RulesetLoadException(@"Instantiation failure"); + // If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution. + // To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw. + resolvedType.Assembly.GetTypes(); + r.Name = instanceInfo.Name; r.ShortName = instanceInfo.ShortName; r.InstantiationInfo = instanceInfo.InstantiationInfo; From e7d72f1823a5a97db761b0712851128942668366 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 15:10:56 +0900 Subject: [PATCH 376/587] Revert recent changes --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 80 +++++++++++++++---- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index a383b533fd..cfe3312415 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -11,8 +11,8 @@ using osu.Game.Rulesets.Osu.UI; using osuTK; using System.Linq; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Configuration; using osu.Framework.Bindables; +using osu.Game.Configuration; namespace osu.Game.Rulesets.Osu.Mods { @@ -34,10 +34,16 @@ namespace osu.Game.Rulesets.Osu.Mods MaxValue = 1.0f, }; - private double? lastUpdate; + private const float spin_radius = 30; + + private Vector2? prevCursorPos; + private OsuInputManager inputManager; public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { + // Grab the input manager for future use + inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; + // Hide judgment displays and follow points drawableRuleset.Playfield.DisplayJudgements.Value = false; (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); @@ -45,29 +51,75 @@ namespace osu.Game.Rulesets.Osu.Mods public void Update(Playfield playfield) { + var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; double currentTime = playfield.Clock.CurrentTime; - if (currentTime - (lastUpdate ?? double.MinValue) < 100) - return; - - Vector2 cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; - + // Move all currently alive object to new destination foreach (var drawable in playfield.HitObjectContainer.AliveObjects.OfType()) { var h = drawable.HitObject; - if (currentTime < h.StartTime && (drawable is DrawableHitCircle || drawable is DrawableSlider)) + switch (drawable) { - double timeMoving = currentTime - (h.StartTime - h.TimePreempt); - float percentDoneMoving = (float)(timeMoving / h.TimePreempt); - float percentDistLeft = Math.Clamp(AssistStrength.Value - percentDoneMoving + 0.1f, 0, 1); + case DrawableHitCircle circle: - Vector2 targetPos = drawable.Position + percentDistLeft * (cursorPos - drawable.Position); - drawable.MoveTo(targetPos, h.StartTime - currentTime); + // 10ms earlier on the note to reduce chance of missing when clicking early / cursor moves fast + circle.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); + // FIXME: some circles cause flash at original(?) position when clicked too early + + break; + + case DrawableSlider slider: + + // Move slider to cursor + if (currentTime < h.StartTime) + { + slider.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); + } + // Move slider so that sliderball stays on the cursor + else + { + slider.HeadCircle.Hide(); // hide flash, triangles, ... so they don't move with slider + slider.MoveTo(cursorPos - slider.Ball.DrawPosition); + // FIXME: some sliders re-appearing at their original position for a single frame when they're done + } + + break; + + case DrawableSpinner spinner: + + // Move spinner _next_ to cursor + if (currentTime < h.StartTime) + { + spinner.MoveTo(cursorPos + new Vector2(0, -spin_radius), Math.Max(0, h.StartTime - currentTime - 10)); + } + else + { + // Move spinner visually + Vector2 delta = spin_radius * (spinner.Position - prevCursorPos ?? cursorPos).Normalized(); + const float angle = 3 * MathF.PI / 180; // radians per update, arbitrary value + + // Rotation matrix + var targetPos = new Vector2( + delta.X * MathF.Cos(angle) - delta.Y * MathF.Sin(angle) + cursorPos.X, + delta.X * MathF.Sin(angle) + delta.Y * MathF.Cos(angle) + cursorPos.Y + ); + + spinner.MoveTo(targetPos); + + // Move spinner logically + if (inputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false) + { + // Arbitrary value, might lead to some inconsistencies depending on clock rate, replay, ... + spinner.RotationTracker.AddRotation(2 * MathF.PI); + } + } + + break; } } - lastUpdate = currentTime; + prevCursorPos = cursorPos; } } } From 104256a054150963b6ffd5a4b68c14fedcfff834 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 15:06:04 +0900 Subject: [PATCH 377/587] Add test coverage --- .../Mods/TestSceneOsuModAimAssist.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs new file mode 100644 index 0000000000..8fa7e0cd09 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModAimAssist : OsuModTestScene + { + [Test] + public void TestAimAssist() + { + var mod = new OsuModAimAssist(); + + CreateModTest(new ModTestData + { + Autoplay = false, + Mod = mod, + }); + } + } +} From 334ed2c9c488ec63e786d9197f756aa5bdaeb3b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 15:36:09 +0900 Subject: [PATCH 378/587] Fix sliders moving before they are actually hit --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index cfe3312415..a3c5638ea7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods case DrawableSlider slider: // Move slider to cursor - if (currentTime < h.StartTime) + if (!slider.HeadCircle.Result.HasResult) { slider.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); } From f07502ac5fadab49871be06c51bb29d95a7cb32a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 16:15:06 +0900 Subject: [PATCH 379/587] Use simple damp easing rather than transforms --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index a3c5638ea7..db83ee09af 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -2,17 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.UI; -using osu.Game.Rulesets.Osu.UI; -using osuTK; using System.Linq; -using osu.Game.Rulesets.Osu.Objects; using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; +using osuTK; namespace osu.Game.Rulesets.Osu.Mods { @@ -26,17 +27,18 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) }; + private IFrameStableClock gameplayClock; + [SettingSource("Assist strength", "Change the distance notes should travel towards you.", 0)] - public BindableFloat AssistStrength { get; } = new BindableFloat(0.3f) + public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f) { Precision = 0.05f, - MinValue = 0.0f, + MinValue = 0.1f, MaxValue = 1.0f, }; - private const float spin_radius = 30; + private const float spin_radius = 50; - private Vector2? prevCursorPos; private OsuInputManager inputManager; public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) @@ -44,6 +46,8 @@ namespace osu.Game.Rulesets.Osu.Mods // Grab the input manager for future use inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; + gameplayClock = drawableRuleset.FrameStableClock; + // Hide judgment displays and follow points drawableRuleset.Playfield.DisplayJudgements.Value = false; (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); @@ -62,10 +66,7 @@ namespace osu.Game.Rulesets.Osu.Mods switch (drawable) { case DrawableHitCircle circle: - - // 10ms earlier on the note to reduce chance of missing when clicking early / cursor moves fast - circle.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); - // FIXME: some circles cause flash at original(?) position when clicked too early + easeTo(circle, cursorPos); break; @@ -74,13 +75,13 @@ namespace osu.Game.Rulesets.Osu.Mods // Move slider to cursor if (!slider.HeadCircle.Result.HasResult) { - slider.MoveTo(cursorPos, Math.Max(0, h.StartTime - currentTime - 10)); + easeTo(slider, cursorPos); } // Move slider so that sliderball stays on the cursor else { slider.HeadCircle.Hide(); // hide flash, triangles, ... so they don't move with slider - slider.MoveTo(cursorPos - slider.Ball.DrawPosition); + easeTo(slider, cursorPos - slider.Ball.DrawPosition); // FIXME: some sliders re-appearing at their original position for a single frame when they're done } @@ -91,35 +92,39 @@ namespace osu.Game.Rulesets.Osu.Mods // Move spinner _next_ to cursor if (currentTime < h.StartTime) { - spinner.MoveTo(cursorPos + new Vector2(0, -spin_radius), Math.Max(0, h.StartTime - currentTime - 10)); + easeTo(spinner, cursorPos + new Vector2(0, -spin_radius)); } else { // Move spinner visually - Vector2 delta = spin_radius * (spinner.Position - prevCursorPos ?? cursorPos).Normalized(); - const float angle = 3 * MathF.PI / 180; // radians per update, arbitrary value - - // Rotation matrix - var targetPos = new Vector2( - delta.X * MathF.Cos(angle) - delta.Y * MathF.Sin(angle) + cursorPos.X, - delta.X * MathF.Sin(angle) + delta.Y * MathF.Cos(angle) + cursorPos.Y - ); - - spinner.MoveTo(targetPos); + Vector2 delta = new Vector2(spin_radius); + float angle = (float)gameplayClock.CurrentTime * 10; // Move spinner logically if (inputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false) { - // Arbitrary value, might lead to some inconsistencies depending on clock rate, replay, ... - spinner.RotationTracker.AddRotation(2 * MathF.PI); + var targetPos = new Vector2( + delta.X * MathF.Cos(angle) - delta.Y * MathF.Sin(angle) + cursorPos.X, + delta.X * MathF.Sin(angle) + delta.Y * MathF.Cos(angle) + cursorPos.Y + ); + + easeTo(spinner, targetPos); } } break; } } + } - prevCursorPos = cursorPos; + private void easeTo(DrawableHitObject hitObject, Vector2 destination) + { + double dampLength = Interpolation.Lerp(500, 50, AssistStrength.Value); + + float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime); + float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime); + + hitObject.Position = new Vector2(x, y); } } } From 987aa5a21c043360d71f0155b52d70e77ddd3a5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 16:15:14 +0900 Subject: [PATCH 380/587] Add testing of different strengths --- .../Mods/TestSceneOsuModAimAssist.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs index 8fa7e0cd09..b8310bc4e7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs @@ -8,15 +8,19 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModAimAssist : OsuModTestScene { - [Test] - public void TestAimAssist() + [TestCase(0.1f)] + [TestCase(0.5f)] + [TestCase(1)] + public void TestAimAssist(float strength) { - var mod = new OsuModAimAssist(); - CreateModTest(new ModTestData { + Mod = new OsuModAimAssist + { + AssistStrength = { Value = strength }, + }, + PassCondition = () => true, Autoplay = false, - Mod = mod, }); } } From 2e46404fe5e10f5279730ad61f90be5a805830b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 16:15:49 +0900 Subject: [PATCH 381/587] Remove spinner support for now --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index db83ee09af..e3a68bcbcc 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Osu.Mods MaxValue = 1.0f, }; - private const float spin_radius = 50; - private OsuInputManager inputManager; public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) @@ -82,34 +80,6 @@ namespace osu.Game.Rulesets.Osu.Mods { slider.HeadCircle.Hide(); // hide flash, triangles, ... so they don't move with slider easeTo(slider, cursorPos - slider.Ball.DrawPosition); - // FIXME: some sliders re-appearing at their original position for a single frame when they're done - } - - break; - - case DrawableSpinner spinner: - - // Move spinner _next_ to cursor - if (currentTime < h.StartTime) - { - easeTo(spinner, cursorPos + new Vector2(0, -spin_radius)); - } - else - { - // Move spinner visually - Vector2 delta = new Vector2(spin_radius); - float angle = (float)gameplayClock.CurrentTime * 10; - - // Move spinner logically - if (inputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false) - { - var targetPos = new Vector2( - delta.X * MathF.Cos(angle) - delta.Y * MathF.Sin(angle) + cursorPos.X, - delta.X * MathF.Sin(angle) + delta.Y * MathF.Cos(angle) + cursorPos.Y - ); - - easeTo(spinner, targetPos); - } } break; From 6e41a6e704512ea2a355ae23d5793435ce464ef7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 16:26:10 +0900 Subject: [PATCH 382/587] Tidy up code into a presentable state --- osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs index e3a68bcbcc..ed4b139e00 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; @@ -23,30 +22,26 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "AA"; public override IconUsage? Icon => FontAwesome.Solid.MousePointer; public override ModType Type => ModType.Fun; - public override string Description => "No need to chase the circle, the circle chases you"; + public override string Description => "No need to chase the circle – the circle chases you!"; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) }; private IFrameStableClock gameplayClock; - [SettingSource("Assist strength", "Change the distance notes should travel towards you.", 0)] + [SettingSource("Assist strength", "How much this mod will assist you.", 0)] public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f) { Precision = 0.05f, - MinValue = 0.1f, + MinValue = 0.05f, MaxValue = 1.0f, }; - private OsuInputManager inputManager; - public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - // Grab the input manager for future use - inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; - gameplayClock = drawableRuleset.FrameStableClock; - // Hide judgment displays and follow points + // Hide judgment displays and follow points as they won't make any sense. + // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. drawableRuleset.Playfield.DisplayJudgements.Value = false; (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); } @@ -54,33 +49,21 @@ namespace osu.Game.Rulesets.Osu.Mods public void Update(Playfield playfield) { var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; - double currentTime = playfield.Clock.CurrentTime; - // Move all currently alive object to new destination - foreach (var drawable in playfield.HitObjectContainer.AliveObjects.OfType()) + foreach (var drawable in playfield.HitObjectContainer.AliveObjects) { - var h = drawable.HitObject; - switch (drawable) { case DrawableHitCircle circle: easeTo(circle, cursorPos); - break; case DrawableSlider slider: - // Move slider to cursor if (!slider.HeadCircle.Result.HasResult) - { easeTo(slider, cursorPos); - } - // Move slider so that sliderball stays on the cursor else - { - slider.HeadCircle.Hide(); // hide flash, triangles, ... so they don't move with slider easeTo(slider, cursorPos - slider.Ball.DrawPosition); - } break; } @@ -89,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods private void easeTo(DrawableHitObject hitObject, Vector2 destination) { - double dampLength = Interpolation.Lerp(500, 50, AssistStrength.Value); + double dampLength = Interpolation.Lerp(3000, 40, AssistStrength.Value); float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime); float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime); From 4758de226beda4f592aecef6f2ec2d8d0c390f03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 16:27:59 +0900 Subject: [PATCH 383/587] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index f85a96f819..963b61a2d3 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index aa6fb93aa0..cc68393eae 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index fbb4688588..846617c2f7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + From 5e3d124eef996801dcb315cc48ed7499dc4aa0ef Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 17:20:22 +0800 Subject: [PATCH 384/587] Add scrolling to the extended statistics panel --- .../Ranking/Statistics/StatisticsPanel.cs | 78 +++++++++++++------ 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 9d89aa213c..1c96e640b4 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; using osu.Game.Scoring; @@ -85,15 +86,21 @@ namespace osu.Game.Screens.Ranking.Statistics playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); }, loadCancellation.Token).ContinueWith(t => Schedule(() => { - var rows = new FillFlowContainer + FillFlowContainer rows; + Container container = new OsuScrollContainer(Direction.Vertical) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Direction = FillDirection.Vertical, - Spacing = new Vector2(30, 15), - Alpha = 0 + Alpha = 0, + Children = new[] + { + rows = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } }; bool panelIsEmpty = true; @@ -107,6 +114,8 @@ namespace osu.Game.Screens.Ranking.Statistics if (columnsToDisplay?.Any() ?? false) panelIsEmpty = false; + else + continue; rows.Add(new GridContainer { @@ -114,6 +123,7 @@ namespace osu.Game.Screens.Ranking.Statistics Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 15 }, Content = new[] { columnsToDisplay?.Select(c => new StatisticContainer(c) @@ -130,27 +140,51 @@ namespace osu.Game.Screens.Ranking.Statistics if (!hitEventsAvailable) { - rows.Add(new FillFlowContainer + if (panelIsEmpty) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] + // Replace the scroll container with fill flow container to get the message centered. + container = new FillFlowContainer { - new MessagePlaceholder(panelIsEmpty - ? "Extended statistics are only available after watching a replay!" - : "More statistics available after watching a replay!"), - new ReplayDownloadButton(newScore) + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Scale = new Vector2(1.5f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - } - }); + new MessagePlaceholder("Extended statistics are only available after watching a replay!"), + new ReplayDownloadButton(newScore) + { + Scale = new Vector2(1.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }; + } + else + { + rows.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Children = new Drawable[] + { + new MessagePlaceholder("More statistics available after watching a replay!"), + new ReplayDownloadButton(newScore) + { + Scale = new Vector2(1.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }); + } } - LoadComponentAsync(rows, d => + LoadComponentAsync(container, d => { if (!Score.Value.Equals(newScore)) return; From 6a482827fea64cbc0869700489e4feb9389f86d6 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 17:23:03 +0800 Subject: [PATCH 385/587] Fix weird line breaking --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 6 ++---- osu.Game.Rulesets.Osu/OsuRuleset.cs | 9 +++------ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 6 ++---- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 8ac8001457..0ea1cd5fc7 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -370,8 +370,7 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem("Timing Distribution", - true, + new StatisticItem("Timing Distribution", true, () => new HitEventTimingDistributionGraph(score.HitEvents) { RelativeSizeAxes = Axes.X, @@ -383,8 +382,7 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem(string.Empty, - true, + new StatisticItem(string.Empty, true, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new UnstableRate(score.HitEvents) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index b6f417b7fe..2bf47100c1 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -277,8 +277,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Timing Distribution", - true, + new StatisticItem("Timing Distribution", true, () => new HitEventTimingDistributionGraph(timedHitEvents) { RelativeSizeAxes = Axes.X, @@ -290,8 +289,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Accuracy Heatmap", - true, + new StatisticItem("Accuracy Heatmap", true, () => new AccuracyHeatmap(score, playableBeatmap) { RelativeSizeAxes = Axes.X, @@ -303,8 +301,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem(string.Empty, - true, + new StatisticItem(string.Empty, true, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new UnstableRate(timedHitEvents) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 2a64b8dddd..fe4116b4a6 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -213,8 +213,7 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem("Timing Distribution", - true, + new StatisticItem("Timing Distribution", true, () => new HitEventTimingDistributionGraph(timedHitEvents) { RelativeSizeAxes = Axes.X, @@ -226,8 +225,7 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem(string.Empty, - true, + new StatisticItem(string.Empty, true, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new UnstableRate(timedHitEvents) From 90e30bc9e8b3e7058aebfa83336383d52d0e0efb Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 17:26:17 +0800 Subject: [PATCH 386/587] Remove useless null checks --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 1c96e640b4..8b69f83055 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -110,9 +110,9 @@ namespace osu.Game.Screens.Ranking.Statistics { var columnsToDisplay = hitEventsAvailable ? row.Columns - : row.Columns?.Where(c => !c.RequiresHitEvents).ToArray(); + : row.Columns.Where(c => !c.RequiresHitEvents).ToArray(); - if (columnsToDisplay?.Any() ?? false) + if (columnsToDisplay.Any()) panelIsEmpty = false; else continue; @@ -132,8 +132,8 @@ namespace osu.Game.Screens.Ranking.Statistics Origin = Anchor.Centre, }).Cast().ToArray() }, - ColumnDimensions = Enumerable.Range(0, columnsToDisplay?.Length ?? 0) - .Select(i => columnsToDisplay?[i].Dimension ?? new Dimension()).ToArray(), + ColumnDimensions = Enumerable.Range(0, columnsToDisplay.Length) + .Select(i => columnsToDisplay[i].Dimension ?? new Dimension()).ToArray(), RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } From 042574660c10d6a28c29a81d6f45f5b8547f956a Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 17:29:03 +0800 Subject: [PATCH 387/587] Rename "Content" to "CreateContent" --- .../Ranking/Statistics/StatisticContainer.cs | 2 +- .../Screens/Ranking/Statistics/StatisticItem.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index f3bd2c6fc1..79f813ef64 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 15 }, - Child = item.Content() + Child = item.CreateContent() } }, }, diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index 5e6ddf445b..cb5ba4b9fe 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -19,9 +19,9 @@ namespace osu.Game.Screens.Ranking.Statistics public readonly string Name; /// - /// The content to be displayed. + /// A function returning the content to be displayed. /// - public readonly Func Content; + public readonly Func CreateContent; /// /// The of this row. This can be thought of as the column dimension of an encompassing . @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Ranking.Statistics public readonly Dimension Dimension; /// - /// Whether this item requires hit events. If true, the will not be created if no hit events are available. + /// Whether this item requires hit events. If true, will not be called if no hit events are available. /// public readonly bool RequiresHitEvents; @@ -37,14 +37,14 @@ namespace osu.Game.Screens.Ranking.Statistics /// Creates a new , to be displayed inside a in the results screen. /// /// The name of the item. Can be to hide the item header. - /// Whether this item requires hit events. If true, the will not be created if no hit events are available. - /// The content to be displayed. + /// Whether this item requires hit events. If true, will not be called if no hit events are available. + /// A function returning the content to be displayed. /// The of this item. This can be thought of as the column dimension of an encompassing . - public StatisticItem([NotNull] string name, bool requiresHitEvents, [NotNull] Func content, [CanBeNull] Dimension dimension = null) + public StatisticItem([NotNull] string name, bool requiresHitEvents, [NotNull] Func createContent, [CanBeNull] Dimension dimension = null) { Name = name; RequiresHitEvents = requiresHitEvents; - Content = content; + CreateContent = createContent; Dimension = dimension; } } From 36bfef4f54f4f116ec7f84747db2c4c93a883d0d Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 17:32:16 +0800 Subject: [PATCH 388/587] Dispose container before replacing --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 8b69f83055..2327f60f81 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -143,6 +143,7 @@ namespace osu.Game.Screens.Ranking.Statistics if (panelIsEmpty) { // Replace the scroll container with fill flow container to get the message centered. + container.Dispose(); container = new FillFlowContainer { RelativeSizeAxes = Axes.Both, From b5fb3b7dae02bdbaeec37e535d447bb09fcfbaeb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 18:42:20 +0900 Subject: [PATCH 389/587] Fix crash when selecting swap mod as freemod --- .../TestSceneMultiplayerMatchSubScreen.cs | 25 +++++++++++++++++++ .../Participants/ParticipantPanel.cs | 8 ++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 9d14d80d07..869fb17317 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -15,8 +15,12 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Rulesets.UI; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -77,6 +81,27 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); } + [Test] + public void TestTaikoOnlyMod() + { + AddStep("add playlist item", () => + { + SelectedRoom.Value.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo }, + Ruleset = { Value = new TaikoRuleset().RulesetInfo }, + AllowedMods = { new TaikoModSwap() } + }); + }); + + ClickButtonWhenEnabled(); + + AddUntilStep("wait for join", () => RoomJoined); + + AddStep("select swap mod", () => Client.ChangeUserMods(API.LocalUser.Value.OnlineID, new[] { new TaikoModSwap() })); + AddUntilStep("participant panel has mod", () => this.ChildrenOfType().Any(p => p.ChildrenOfType().Any(m => m.Mod is TaikoModSwap))); + } + [Test] public void TestSettingValidity() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 8fbaebadfe..96a665f33d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -18,6 +19,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Screens.Play.HUD; using osu.Game.Users; @@ -184,8 +186,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; - // Todo: Should use the room's selected item to determine ruleset. - var ruleset = rulesets.GetRuleset(0)?.CreateInstance(); + var currentItem = Playlist.GetCurrentItem(); + Debug.Assert(currentItem != null); + + var ruleset = rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance(); int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; From b0023b9809d15cdb7f19d5a2f6be9c0d047bf9fc Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 19:00:46 +0800 Subject: [PATCH 390/587] Also dispose `rows` --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 2327f60f81..62f9eb8506 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -143,6 +143,7 @@ namespace osu.Game.Screens.Ranking.Statistics if (panelIsEmpty) { // Replace the scroll container with fill flow container to get the message centered. + rows.Dispose(); container.Dispose(); container = new FillFlowContainer { From 1e19c7046a71de056cc975deaa045eb2eff46c8b Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 19:02:29 +0800 Subject: [PATCH 391/587] Use spacing instead of bottom margin --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 62f9eb8506..68414c4d49 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -98,7 +98,8 @@ namespace osu.Game.Screens.Ranking.Statistics rows = new FillFlowContainer { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(30, 15) } } }; @@ -123,7 +124,6 @@ namespace osu.Game.Screens.Ranking.Statistics Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 15 }, Content = new[] { columnsToDisplay?.Select(c => new StatisticContainer(c) From 3c2a6fe2086a1eb560a4aefe1805a8d402b1d153 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Feb 2022 19:07:14 +0800 Subject: [PATCH 392/587] Don't prompt for a replay if no item requires hit events --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 68414c4d49..08be1de652 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -105,6 +105,7 @@ namespace osu.Game.Screens.Ranking.Statistics }; bool panelIsEmpty = true; + bool panelIsComplete = true; bool hitEventsAvailable = newScore.HitEvents.Count != 0; foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) @@ -113,6 +114,9 @@ namespace osu.Game.Screens.Ranking.Statistics ? row.Columns : row.Columns.Where(c => !c.RequiresHitEvents).ToArray(); + if (columnsToDisplay.Length < row.Columns.Length) + panelIsComplete = false; + if (columnsToDisplay.Any()) panelIsEmpty = false; else @@ -163,7 +167,7 @@ namespace osu.Game.Screens.Ranking.Statistics } }; } - else + else if (!panelIsComplete) { rows.Add(new FillFlowContainer { From 19eb9ad8a7e86b384ca65648b95e35e37c2ce068 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Feb 2022 23:02:38 +0900 Subject: [PATCH 393/587] Reorder `StatisticsItem` constructor to make a touch more sense --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 20 ++++++------ osu.Game.Rulesets.Osu/OsuRuleset.cs | 31 +++++++++---------- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 20 ++++++------ .../Ranking/Statistics/StatisticItem.cs | 4 +-- 4 files changed, 34 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 0ea1cd5fc7..ffb26b224f 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -370,23 +370,21 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem("Timing Distribution", true, - () => new HitEventTimingDistributionGraph(score.HitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, true, - () => new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(score.HitEvents) - })) + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(score.HitEvents) + }), true) } } }; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 2bf47100c1..1122a869b7 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -277,35 +277,32 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Timing Distribution", true, - () => new HitEventTimingDistributionGraph(timedHitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), } }, new StatisticRow { Columns = new[] { - new StatisticItem("Accuracy Heatmap", true, - () => new AccuracyHeatmap(score, playableBeatmap) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, true, - () => new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(timedHitEvents) - })) + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + }), true) } } }; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index fe4116b4a6..21c99c0d2f 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -213,23 +213,21 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem("Timing Distribution", true, - () => new HitEventTimingDistributionGraph(timedHitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), } }, new StatisticRow { Columns = new[] { - new StatisticItem(string.Empty, true, - () => new SimpleStatisticTable(3, new SimpleStatisticItem[] - { - new UnstableRate(timedHitEvents) - })) + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + }), true) } } }; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index cb5ba4b9fe..3cfc38e263 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -37,10 +37,10 @@ namespace osu.Game.Screens.Ranking.Statistics /// Creates a new , to be displayed inside a in the results screen. /// /// The name of the item. Can be to hide the item header. - /// Whether this item requires hit events. If true, will not be called if no hit events are available. /// A function returning the content to be displayed. + /// Whether this item requires hit events. If true, will not be called if no hit events are available. /// The of this item. This can be thought of as the column dimension of an encompassing . - public StatisticItem([NotNull] string name, bool requiresHitEvents, [NotNull] Func createContent, [CanBeNull] Dimension dimension = null) + public StatisticItem([NotNull] string name, [NotNull] Func createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null) { Name = name; RequiresHitEvents = requiresHitEvents; From 9d1d13c7152592d31a212fc04b04aac1fac171b1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:00:43 +0900 Subject: [PATCH 394/587] Fix up TestSpectatorClient implementation Rather than using a list which is supposed to be updated "client"-side, now uses the "server"-side list. --- .../Visual/Spectator/TestSpectatorClient.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 7848a825f4..6862cda88c 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -39,11 +39,7 @@ namespace osu.Game.Tests.Visual.Spectator public TestSpectatorClient() { - OnNewFrames += (i, bundle) => - { - if (PlayingUserStates.ContainsKey(i)) - lastReceivedUserFrames[i] = bundle.Frames[^1]; - }; + OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1]; } /// @@ -62,16 +58,20 @@ namespace osu.Game.Tests.Visual.Spectator /// Ends play for an arbitrary user. /// /// The user to end play for. - public void EndPlay(int userId) + /// The spectator state to end play with. + public void EndPlay(int userId, SpectatingUserState state = SpectatingUserState.Quit) { - if (!PlayingUserStates.ContainsKey(userId)) + if (!userBeatmapDictionary.ContainsKey(userId)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, + State = state }); + + userBeatmapDictionary.Remove(userId); } public new void Schedule(Action action) => base.Schedule(action); @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Spectator protected override Task WatchUserInternal(int userId) { // When newly watching a user, the server sends the playing state immediately. - if (PlayingUserStates.ContainsKey(userId)) + if (userBeatmapDictionary.ContainsKey(userId)) sendPlayingState(userId); return Task.CompletedTask; @@ -144,6 +144,7 @@ namespace osu.Game.Tests.Visual.Spectator { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, + State = SpectatingUserState.Playing }); } } From 589f5e7a31f53275f7cd001adbe4deedb1e18473 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:09:38 +0900 Subject: [PATCH 395/587] Update test which has now been resolved --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index d28e5203e3..9f8470446c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -155,11 +155,12 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); checkPaused(true); + sendFrames(); - finish(); + finish(SpectatingUserState.Failed); - checkPaused(false); - // TODO: should replay until running out of frames then fail + checkPaused(false); // Should continue playing until out of frames + checkPaused(true); } [Test] @@ -246,7 +247,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); - private void finish() => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id)); + private void finish(SpectatingUserState state = SpectatingUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state)); private void checkPaused(bool state) => AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); From fcbba3d9481ca396acd9885593065ea782362475 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:11:29 +0900 Subject: [PATCH 396/587] Rename PlayingUserStates -> WatchingUserStates --- .../Visual/Gameplay/TestSceneSpectatorHost.cs | 4 ++-- .../TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 12 ++++++------ .../Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs index 409cec4cf6..6d6b0bf89e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs @@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClientSendsCorrectRuleset() { - AddUntilStep("spectator client sending frames", () => spectatorClient.PlayingUserStates.ContainsKey(dummy_user_id)); - AddAssert("spectator client sent correct ruleset", () => spectatorClient.PlayingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); + AddUntilStep("spectator client sending frames", () => spectatorClient.WatchingUserStates.ContainsKey(dummy_user_id)); + AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); } public override void TearDownSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 8b71dfac21..55450b36e2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void RandomlyUpdateState() { - foreach ((int userId, _) in PlayingUserStates) + foreach ((int userId, _) in WatchingUserStates) { if (RNG.NextBool()) continue; diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 156d544960..3646c51d94 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -37,8 +37,8 @@ namespace osu.Game.Online.Spectator private readonly List watchingUsers = new List(); - public IBindableDictionary PlayingUserStates => playingUserStates; - private readonly BindableDictionary playingUserStates = new BindableDictionary(); + public IBindableDictionary WatchingUserStates => watchingUserStates; + private readonly BindableDictionary watchingUserStates = new BindableDictionary(); private IBeatmap? currentBeatmap; private Score? currentScore; @@ -85,7 +85,7 @@ namespace osu.Game.Online.Spectator BeginPlayingInternal(currentState); } else - playingUserStates.Clear(); + watchingUserStates.Clear(); }), true); } @@ -94,7 +94,7 @@ namespace osu.Game.Online.Spectator Schedule(() => { if (watchingUsers.Contains(userId)) - playingUserStates[userId] = state; + watchingUserStates[userId] = state; OnUserBeganPlaying?.Invoke(userId, state); }); @@ -106,7 +106,7 @@ namespace osu.Game.Online.Spectator Schedule(() => { if (watchingUsers.Contains(userId)) - playingUserStates[userId] = state; + watchingUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); }); @@ -193,7 +193,7 @@ namespace osu.Game.Online.Spectator Schedule(() => { watchingUsers.Remove(userId); - playingUserStates.Remove(userId); + watchingUserStates.Remove(userId); StopWatchingUserInternal(userId); }); } diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 383f17d8d2..975402a443 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindTo(spectatorClient.WatchingUserStates); userStates.BindCollectionChanged(onUserStatesChanged, true); } diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 783ba494eb..0b2a7cecf6 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Spectate userMap[u.Id] = u; } - userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindTo(spectatorClient.WatchingUserStates); userStates.BindCollectionChanged(onUserStatesChanged, true); realmSubscription = realm.RegisterForNotifications( From 81a22dbd29543b3ff5387669bd6ce1fc048a4811 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:19:43 +0900 Subject: [PATCH 397/587] Add back playing users list --- osu.Game/Online/Spectator/SpectatorClient.cs | 28 +++++++++-- .../Dashboard/CurrentlyPlayingDisplay.cs | 48 ++++++++----------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 3646c51d94..9e168411b0 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -35,16 +35,28 @@ namespace osu.Game.Online.Spectator /// public abstract IBindable IsConnected { get; } + /// + /// The states of all users currently being watched. + /// + public IBindableDictionary WatchingUserStates => watchingUserStates; + + /// + /// A global list of all players currently playing. + /// + public IBindableList PlayingUsers => playingUsers; + + /// + /// All users currently being watched. + /// private readonly List watchingUsers = new List(); - public IBindableDictionary WatchingUserStates => watchingUserStates; private readonly BindableDictionary watchingUserStates = new BindableDictionary(); + private readonly BindableList playingUsers = new BindableList(); + private readonly SpectatorState currentState = new SpectatorState(); private IBeatmap? currentBeatmap; private Score? currentScore; - private readonly SpectatorState currentState = new SpectatorState(); - /// /// Whether the local user is playing. /// @@ -85,7 +97,10 @@ namespace osu.Game.Online.Spectator BeginPlayingInternal(currentState); } else + { watchingUserStates.Clear(); + playingUsers.Clear(); + } }), true); } @@ -93,8 +108,12 @@ namespace osu.Game.Online.Spectator { Schedule(() => { + if (!playingUsers.Contains(userId)) + playingUsers.Add(userId); + if (watchingUsers.Contains(userId)) watchingUserStates[userId] = state; + OnUserBeganPlaying?.Invoke(userId, state); }); @@ -105,8 +124,11 @@ namespace osu.Game.Online.Spectator { Schedule(() => { + playingUsers.Remove(userId); + if (watchingUsers.Contains(userId)) watchingUserStates[userId] = state; + OnUserFinishedPlaying?.Invoke(userId, state); }); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 975402a443..02ef28f825 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private readonly IBindableDictionary userStates = new BindableDictionary(); + private readonly IBindableList playingUsers = new BindableList(); private FillFlowContainer userFlow; @@ -51,55 +52,46 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - userStates.BindTo(spectatorClient.WatchingUserStates); - userStates.BindCollectionChanged(onUserStatesChanged, true); + playingUsers.BindTo(spectatorClient.PlayingUsers); + playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); } - private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { switch (e.Action) { - case NotifyDictionaryChangedAction.Add: - case NotifyDictionaryChangedAction.Replace: + case NotifyCollectionChangedAction.Add: Debug.Assert(e.NewItems != null); - foreach ((int userId, SpectatorState state) in e.NewItems) + foreach (int userId in e.NewItems) { - if (state.State != SpectatingUserState.Playing) - { - removePlayingUser(userId); - continue; - } - users.GetUserAsync(userId).ContinueWith(task => { var user = task.GetResultSafely(); if (user != null) - Schedule(() => addPlayingUser(user)); + { + Schedule(() => + { + // user may no longer be playing. + if (!playingUsers.Contains(user.Id)) + return; + + userFlow.Add(createUserPanel(user)); + }); + } }); } break; - case NotifyDictionaryChangedAction.Remove: + case NotifyCollectionChangedAction.Remove: Debug.Assert(e.OldItems != null); - foreach ((int userId, _) in e.OldItems) - removePlayingUser(userId); + foreach (int userId in e.OldItems) + userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); break; } - - void addPlayingUser(APIUser user) - { - // user may no longer be playing. - if (!userStates.TryGetValue(user.Id, out var state2) || state2.State != SpectatingUserState.Playing) - return; - - userFlow.Add(createUserPanel(user)); - } - - void removePlayingUser(int userId) => userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); }); private PlayingUserPanel createUserPanel(APIUser user) => From 074a69163558c6ee6c2b177e97ed53126ab8b805 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 2 Feb 2022 17:16:35 +0300 Subject: [PATCH 398/587] Set keyboard step to `0.1` for difficulty adjust sliders --- osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs index 45873a321a..c8e7284f5d 100644 --- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs +++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs @@ -105,6 +105,7 @@ namespace osu.Game.Rulesets.Mods { ShowsDefaultIndicator = false, Current = currentNumber, + KeyboardStep = 0.1f, } }; From 74637444070838cc55bd0d7e04f9580f13552737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 2 Feb 2022 19:17:33 +0100 Subject: [PATCH 399/587] Fix osu! autoplay-like mods not declaring incompatibility with `AimAssist` --- osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index 106edfb623..2668013321 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModAutoplay : ModAutoplay { - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs index f478790134..ff31cfcd18 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModCinema : ModCinema { - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { From dca1bddabb789867e34562e20bba19d02e22fab1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 2 Feb 2022 21:25:50 +0300 Subject: [PATCH 400/587] Lock supported interface orientation to landscape for iPhone --- osu.Game.Rulesets.Catch.Tests.iOS/Info.plist | 5 +++++ osu.Game.Rulesets.Mania.Tests.iOS/Info.plist | 5 +++++ osu.Game.Rulesets.Osu.Tests.iOS/Info.plist | 5 +++++ osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist | 5 +++++ osu.Game.Tests.iOS/Info.plist | 5 +++++ osu.iOS/Info.plist | 5 +++++ 6 files changed, 30 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist index 3ba1886d98..88317b6393 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist @@ -24,6 +24,11 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist index 09ed2dd007..8d05c2bb73 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist @@ -24,6 +24,11 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist index dd032ef1c1..63ad313a70 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist @@ -24,6 +24,11 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist index ac658cd14e..3c76622ee3 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist @@ -24,6 +24,11 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist index 1a89345bc5..835e958bc6 100644 --- a/osu.Game.Tests.iOS/Info.plist +++ b/osu.Game.Tests.iOS/Info.plist @@ -24,6 +24,11 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 2592f909ce..1156c0954c 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -40,6 +40,11 @@ NSMicrophoneUsageDescription We don't really use the microphone. UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown From 4aa4df69f258bd4027c8c90abba8adaa6a3a7b28 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 2 Feb 2022 22:17:23 +0300 Subject: [PATCH 401/587] Reorder iOS landscape orientations to prioritise "Landscape Right" "Landscape Right" is often the proper default for landscape-only applications. Matches up with all other landscape-only iOS games I have locally. --- osu.Game.Rulesets.Catch.Tests.iOS/Info.plist | 4 ++-- osu.Game.Rulesets.Mania.Tests.iOS/Info.plist | 4 ++-- osu.Game.Rulesets.Osu.Tests.iOS/Info.plist | 4 ++-- osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist | 4 ++-- osu.Game.Tests.iOS/Info.plist | 4 ++-- osu.iOS/Info.plist | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist index 88317b6393..33ddac6dfb 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist @@ -25,15 +25,15 @@ UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist index 8d05c2bb73..78349334b4 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist @@ -25,15 +25,15 @@ UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist index 63ad313a70..b9f371c049 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist @@ -25,15 +25,15 @@ UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist index 3c76622ee3..65c47d2115 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist @@ -25,15 +25,15 @@ UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist index 835e958bc6..ed0c2e4dbf 100644 --- a/osu.Game.Tests.iOS/Info.plist +++ b/osu.Game.Tests.iOS/Info.plist @@ -25,15 +25,15 @@ UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 1156c0954c..02968b87a7 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -41,15 +41,15 @@ We don't really use the microphone. UISupportedInterfaceOrientations - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset From 82f9ad63f514f1220c44bb087d58a154a1e1f2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 2 Feb 2022 20:41:25 +0100 Subject: [PATCH 402/587] Fix flashlight size multiplier printing with too many decimal digits --- osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs | 2 +- osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs | 2 +- osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs | 2 +- osu.Game/Rulesets/Mods/ModFlashlight.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index 2d92c925d7..d576ea3df8 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods public override double ScoreMultiplier => 1.12; [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] - public override BindableNumber SizeMultiplier { get; } = new BindableNumber + public override BindableFloat SizeMultiplier { get; } = new BindableFloat { MinValue = 0.5f, MaxValue = 1.5f, diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index 1ee4ea12e3..8ef5bfd94c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override Type[] IncompatibleMods => new[] { typeof(ModHidden) }; [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] - public override BindableNumber SizeMultiplier { get; } = new BindableNumber + public override BindableFloat SizeMultiplier { get; } = new BindableFloat { MinValue = 0.5f, MaxValue = 3f, diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index b4eff57c55..38c84be295 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Mods }; [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] - public override BindableNumber SizeMultiplier { get; } = new BindableNumber + public override BindableFloat SizeMultiplier { get; } = new BindableFloat { MinValue = 0.5f, MaxValue = 2f, diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index fb07c687bb..beec785fe8 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public override double ScoreMultiplier => 1.12; [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] - public override BindableNumber SizeMultiplier { get; } = new BindableNumber + public override BindableFloat SizeMultiplier { get; } = new BindableFloat { MinValue = 0.5f, MaxValue = 1.5f, diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index e6487c6b29..b449f3f64d 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Restricted view area."; [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] - public abstract BindableNumber SizeMultiplier { get; } + public abstract BindableFloat SizeMultiplier { get; } [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] public abstract BindableBool ComboBasedSize { get; } From e2fcdc394b25cbeb0af8a74c4e68bd79f722f8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 16:29:00 +0100 Subject: [PATCH 403/587] Extract method for difficulty switch menu creation --- osu.Game/Screens/Edit/Editor.cs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2fead84deb..71a19c4425 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -806,6 +806,15 @@ namespace osu.Game.Screens.Edit fileMenuItems.Add(new EditorMenuItemSpacer()); + fileMenuItems.Add(createDifficultySwitchMenu()); + + fileMenuItems.Add(new EditorMenuItemSpacer()); + fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); + return fileMenuItems; + } + + private EditorMenuItem createDifficultySwitchMenu() + { var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet; Debug.Assert(beatmapSet != null); @@ -818,20 +827,13 @@ namespace osu.Game.Screens.Edit difficultyItems.Add(new EditorMenuItemSpacer()); foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating)) - difficultyItems.Add(createDifficultyMenuItem(beatmap)); + { + bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmap); + difficultyItems.Add(new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty)); + } } - fileMenuItems.Add(new EditorMenuItem("Change difficulty") { Items = difficultyItems }); - - fileMenuItems.Add(new EditorMenuItemSpacer()); - fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); - return fileMenuItems; - } - - private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo) - { - bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmapInfo); - return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, SwitchToDifficulty); + return new EditorMenuItem("Change difficulty") { Items = difficultyItems }; } protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, GetState(nextBeatmap)); From 3386f038ba0857f47934c438d7d8a7024e06285c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 16:34:02 +0100 Subject: [PATCH 404/587] Add new difficulty creation menu --- osu.Game/Screens/Edit/Editor.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 71a19c4425..61c5fd2ca4 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -77,6 +77,9 @@ namespace osu.Game.Screens.Edit [Resolved] private BeatmapManager beatmapManager { get; set; } + [Resolved] + private RulesetStore rulesets { get; set; } + [Resolved] private Storage storage { get; set; } @@ -806,6 +809,7 @@ namespace osu.Game.Screens.Edit fileMenuItems.Add(new EditorMenuItemSpacer()); + fileMenuItems.Add(createDifficultyCreationMenu()); fileMenuItems.Add(createDifficultySwitchMenu()); fileMenuItems.Add(new EditorMenuItemSpacer()); @@ -813,6 +817,16 @@ namespace osu.Game.Screens.Edit return fileMenuItems; } + private EditorMenuItem createDifficultyCreationMenu() + { + var rulesetItems = new List(); + + foreach (var ruleset in rulesets.AvailableRulesets.OrderBy(ruleset => ruleset.OnlineID)) + rulesetItems.Add(new EditorMenuItem(ruleset.Name)); + + return new EditorMenuItem("Create new difficulty") { Items = rulesetItems }; + } + private EditorMenuItem createDifficultySwitchMenu() { var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet; From b613aedeb81ea46bcb664b9b76feac179252778f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 16:36:18 +0100 Subject: [PATCH 405/587] Fix menu item width changing when hovered --- osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index 4267b82bb7..4ecc543ffd 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -117,6 +117,7 @@ namespace osu.Game.Graphics.UserInterface { NormalText = new OsuSpriteText { + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: text_size), @@ -124,7 +125,7 @@ namespace osu.Game.Graphics.UserInterface }, BoldText = new OsuSpriteText { - AlwaysPresent = true, + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. Alpha = 0, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, From dc96c4888bfa5fa04ddd4228b9bf218d206a71c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 17:49:17 +0100 Subject: [PATCH 406/587] Add support for creating new blank difficulties --- osu.Game/Beatmaps/BeatmapManager.cs | 32 +++++++++++++++++++++- osu.Game/Beatmaps/BeatmapMetadata.cs | 16 ++++++++++- osu.Game/Beatmaps/BeatmapModelManager.cs | 23 ++++++++++++++-- osu.Game/Database/RealmObjectExtensions.cs | 11 +++++++- osu.Game/Models/RealmUser.cs | 6 +++- osu.Game/Screens/Edit/Editor.cs | 7 +++-- osu.Game/Screens/Edit/EditorLoader.cs | 11 ++++++-- 7 files changed, 95 insertions(+), 11 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e4fdb3d471..38ba244f28 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -73,7 +73,9 @@ namespace osu.Game.Beatmaps new BeatmapModelManager(realm, storage, onlineLookupQueue); /// - /// Create a new . + /// Create a new beatmap set, backed by a model, + /// with a single difficulty which is backed by a model + /// and represented by the returned usable . /// public WorkingBeatmap CreateNew(RulesetInfo ruleset, APIUser user) { @@ -105,6 +107,34 @@ namespace osu.Game.Beatmaps return imported.PerformRead(s => GetWorkingBeatmap(s.Beatmaps.First())); } + /// + /// Add a new difficulty to the beatmap set represented by the provided . + /// The new difficulty will be backed by a model + /// and represented by the returned . + /// + public WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + { + // fetch one of the existing difficulties to copy timing points and metadata from, + // so that the user doesn't have to fill all of that out again. + // this silently assumes that all difficulties have the same timing points and metadata, + // but cases where this isn't true seem rather rare / pathological. + var referenceBeatmap = GetWorkingBeatmap(beatmapSetInfo.Beatmaps.First()); + + var newBeatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()) + }; + + foreach (var timingPoint in referenceBeatmap.Beatmap.ControlPointInfo.TimingPoints) + newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); + + var createdBeatmapInfo = beatmapModelManager.AddDifficultyToBeatmapSet(beatmapSetInfo, newBeatmap); + return GetWorkingBeatmap(createdBeatmapInfo); + } + + // TODO: add back support for making a copy of another difficulty + // (likely via a separate `CopyDifficulty()` method). + /// /// Delete a beatmap difficulty. /// diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index f6666a6ea9..3a24c4808f 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using osu.Framework.Testing; using osu.Game.Models; using osu.Game.Users; +using osu.Game.Utils; using Realms; #nullable enable @@ -16,7 +17,7 @@ namespace osu.Game.Beatmaps [ExcludeFromDynamicCompile] [Serializable] [MapTo("BeatmapMetadata")] - public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo + public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo, IDeepCloneable { public string Title { get; set; } = string.Empty; @@ -57,5 +58,18 @@ namespace osu.Game.Beatmaps IUser IBeatmapMetadataInfo.Author => Author; public override string ToString() => this.GetDisplayTitle(); + + public BeatmapMetadata DeepClone() => new BeatmapMetadata(Author.DeepClone()) + { + Title = Title, + TitleUnicode = TitleUnicode, + Artist = Artist, + ArtistUnicode = ArtistUnicode, + Source = Source, + Tags = Tags, + PreviewTime = PreviewTime, + AudioFile = AudioFile, + BackgroundFile = BackgroundFile + }; } } diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index e8104f2ecb..2ab5ac1db9 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -49,7 +49,6 @@ namespace osu.Game.Beatmaps public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) { var setInfo = beatmapInfo.BeatmapSet; - Debug.Assert(setInfo != null); // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`. @@ -85,6 +84,24 @@ namespace osu.Game.Beatmaps WorkingBeatmapCache?.Invalidate(beatmapInfo); } + /// + /// Add a new difficulty to the beatmap set represented by the provided . + /// + public BeatmapInfo AddDifficultyToBeatmapSet(BeatmapSetInfo beatmapSetInfo, Beatmap beatmap) + { + return Realm.Run(realm => + { + var beatmapInfo = beatmap.BeatmapInfo; + + beatmapSetInfo.Beatmaps.Add(beatmapInfo); + beatmapInfo.BeatmapSet = beatmapSetInfo; + + Save(beatmapInfo, beatmap); + + return beatmapInfo.Detach(); + }); + } + private static string getFilename(BeatmapInfo beatmapInfo) { var metadata = beatmapInfo.Metadata; @@ -103,9 +120,9 @@ namespace osu.Game.Beatmaps public void Update(BeatmapSetInfo item) { - Realm.Write(realm => + Realm.Write(r => { - var existing = realm.Find(item.ID); + var existing = r.Find(item.ID); item.CopyChangesToRealm(existing); }); } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 7a0ca2c85a..f89bbbe19d 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -58,7 +58,16 @@ namespace osu.Game.Database if (existing != null) copyChangesToRealm(beatmap, existing); else - d.Beatmaps.Add(beatmap); + { + var newBeatmap = new BeatmapInfo + { + ID = beatmap.ID, + BeatmapSet = d, + Ruleset = d.Realm.Find(beatmap.Ruleset.ShortName) + }; + d.Beatmaps.Add(newBeatmap); + copyChangesToRealm(beatmap, newBeatmap); + } } }); diff --git a/osu.Game/Models/RealmUser.cs b/osu.Game/Models/RealmUser.cs index 5fccff597c..18c849cf0a 100644 --- a/osu.Game/Models/RealmUser.cs +++ b/osu.Game/Models/RealmUser.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Game.Database; using osu.Game.Users; +using osu.Game.Utils; using Realms; namespace osu.Game.Models { - public class RealmUser : EmbeddedObject, IUser, IEquatable + public class RealmUser : EmbeddedObject, IUser, IEquatable, IDeepCloneable { public int OnlineID { get; set; } = 1; @@ -22,5 +24,7 @@ namespace osu.Game.Models return OnlineID == other.OnlineID && Username == other.Username; } + + public RealmUser DeepClone() => (RealmUser)this.Detach().MemberwiseClone(); } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 61c5fd2ca4..df8e326c5b 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -822,11 +822,14 @@ namespace osu.Game.Screens.Edit var rulesetItems = new List(); foreach (var ruleset in rulesets.AvailableRulesets.OrderBy(ruleset => ruleset.OnlineID)) - rulesetItems.Add(new EditorMenuItem(ruleset.Name)); + rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => createNewDifficulty(ruleset))); return new EditorMenuItem("Create new difficulty") { Items = rulesetItems }; } + private void createNewDifficulty(RulesetInfo rulesetInfo) + => loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo.BeatmapSet, rulesetInfo, GetState()); + private EditorMenuItem createDifficultySwitchMenu() { var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet; @@ -850,7 +853,7 @@ namespace osu.Game.Screens.Edit return new EditorMenuItem("Change difficulty") { Items = difficultyItems }; } - protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, GetState(nextBeatmap)); + protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap)); private void cancelExit() { diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 15d70e28b6..731bc75b52 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -10,6 +10,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -78,7 +79,13 @@ namespace osu.Game.Screens.Edit } } - public void ScheduleDifficultySwitch(BeatmapInfo nextBeatmap, EditorState editorState) + public void ScheduleSwitchToNewDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo, EditorState editorState) + => scheduleDifficultySwitch(() => beatmapManager.CreateNewBlankDifficulty(beatmapSetInfo, rulesetInfo), editorState); + + public void ScheduleSwitchToExistingDifficulty(BeatmapInfo beatmapInfo, EditorState editorState) + => scheduleDifficultySwitch(() => beatmapManager.GetWorkingBeatmap(beatmapInfo), editorState); + + private void scheduleDifficultySwitch(Func nextBeatmap, EditorState editorState) { scheduledDifficultySwitch?.Cancel(); ValidForResume = true; @@ -87,7 +94,7 @@ namespace osu.Game.Screens.Edit scheduledDifficultySwitch = Schedule(() => { - Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextBeatmap); + Beatmap.Value = nextBeatmap.Invoke(); state = editorState; // This screen is a weird exception to the rule that nothing after song select changes the global beatmap. From 0d51c015addfcd10fdcc24f6ef37446ef7c1fdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 18:34:33 +0100 Subject: [PATCH 407/587] Add basic test coverage for new difficulty creation --- .../Editing/TestSceneEditorBeatmapCreation.cs | 40 +++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 4 +- osu.Game/Tests/Visual/EditorTestScene.cs | 2 + 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index e3fb44534b..9ceff86426 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -90,5 +90,45 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); } + + [Test] + public void TestCreateNewDifficulty() + { + string firstDifficultyName = Guid.NewGuid().ToString(); + string secondDifficultyName = Guid.NewGuid().ToString(); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName); + var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + + return beatmap != null + && beatmap.DifficultyName == firstDifficultyName + && set != null + && set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID); + }); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for created", () => + { + string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != firstDifficultyName; + }); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName); + var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + + return beatmap != null + && beatmap.DifficultyName == secondDifficultyName + && set != null + && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); + }); + } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index df8e326c5b..3d7677ce19 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -822,12 +822,12 @@ namespace osu.Game.Screens.Edit var rulesetItems = new List(); foreach (var ruleset in rulesets.AvailableRulesets.OrderBy(ruleset => ruleset.OnlineID)) - rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => createNewDifficulty(ruleset))); + rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => CreateNewDifficulty(ruleset))); return new EditorMenuItem("Create new difficulty") { Items = rulesetItems }; } - private void createNewDifficulty(RulesetInfo rulesetInfo) + protected void CreateNewDifficulty(RulesetInfo rulesetInfo) => loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo.BeatmapSet, rulesetInfo, GetState()); private EditorMenuItem createDifficultySwitchMenu() diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index bcf169bb1e..52d61ed422 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -107,6 +107,8 @@ namespace osu.Game.Tests.Visual public new void SwitchToDifficulty(BeatmapInfo beatmapInfo) => base.SwitchToDifficulty(beatmapInfo); + public new void CreateNewDifficulty(RulesetInfo rulesetInfo) => base.CreateNewDifficulty(rulesetInfo); + public new bool HasUnsavedChanges => base.HasUnsavedChanges; public TestEditor(EditorLoader loader = null) From 54bb6ad40c992cb55de194ed12aac4686fea6ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 18:56:19 +0100 Subject: [PATCH 408/587] Fix working beatmaps not seeing new difficulties after add --- osu.Game/Beatmaps/BeatmapManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 38ba244f28..d9b0ec3170 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -129,6 +129,8 @@ namespace osu.Game.Beatmaps newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); var createdBeatmapInfo = beatmapModelManager.AddDifficultyToBeatmapSet(beatmapSetInfo, newBeatmap); + + workingBeatmapCache.Invalidate(createdBeatmapInfo.BeatmapSet); return GetWorkingBeatmap(createdBeatmapInfo); } From 87e2e83288ec72a569207d0854dbba49bf4880b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 19:25:59 +0100 Subject: [PATCH 409/587] Add test coverage for difficulty name clash cases --- .../Editing/TestSceneEditorBeatmapCreation.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 9ceff86426..31348c1e17 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -130,5 +130,59 @@ namespace osu.Game.Tests.Visual.Editing && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); }); } + + [Test] + public void TestCreateNewBeatmapFailsWithBlankNamedDifficulties() + { + Guid setId = Guid.Empty; + + AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + }); + + AddStep("try to create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddAssert("beatmap set unchanged", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + }); + } + + [Test] + public void TestCreateNewBeatmapFailsWithSameNamedDifficulties() + { + Guid setId = Guid.Empty; + const string duplicate_difficulty_name = "duplicate"; + + AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + }); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for created", () => + { + string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != duplicate_difficulty_name; + }); + + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); + AddStep("try to save beatmap", () => Editor.Save()); + AddAssert("beatmap set not corrupted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + // the difficulty was already created at the point of the switch. + // what we want to check is that both difficulties do not use the same file. + return set != null && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Files.Count == 2); + }); + } } } From 4f1aac9345f8e4b292d079b630c2a00e62a264ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 19:42:07 +0100 Subject: [PATCH 410/587] Add safeties preventing creating multiple difficulties with same name --- osu.Game/Beatmaps/BeatmapModelManager.cs | 6 ++++++ osu.Game/Screens/Edit/Editor.cs | 16 ++++++++++++---- osu.Game/Screens/Edit/EditorLoader.cs | 16 +++++++++++++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 2ab5ac1db9..327e10e643 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -71,6 +71,12 @@ namespace osu.Game.Beatmaps // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity. var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)); + string targetFilename = getFilename(beatmapInfo); + + // ensure that two difficulties from the set don't point at the same beatmap file. + if (setInfo.Beatmaps.Any(b => string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase))) + throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'."); + if (existingFileInfo != null) DeleteFile(setInfo, existingFileInfo); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3d7677ce19..64b23ccda8 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -386,12 +386,20 @@ namespace osu.Game.Screens.Edit return; } + try + { + // save the loaded beatmap's data stream. + beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin); + } + catch (Exception ex) + { + // can fail e.g. due to duplicated difficulty names. + Logger.Error(ex, ex.Message); + return; + } + // no longer new after first user-triggered save. isNewBeatmap = false; - - // save the loaded beatmap's data stream. - beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin); - updateLastSavedHash(); } diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 731bc75b52..de47411fdc 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -6,6 +6,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; @@ -80,7 +81,20 @@ namespace osu.Game.Screens.Edit } public void ScheduleSwitchToNewDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo, EditorState editorState) - => scheduleDifficultySwitch(() => beatmapManager.CreateNewBlankDifficulty(beatmapSetInfo, rulesetInfo), editorState); + => scheduleDifficultySwitch(() => + { + try + { + return beatmapManager.CreateNewBlankDifficulty(beatmapSetInfo, rulesetInfo); + } + catch (Exception ex) + { + // if the beatmap creation fails (e.g. due to duplicated difficulty names), + // bring the user back to the previous beatmap as a best-effort. + Logger.Error(ex, ex.Message); + return Beatmap.Value; + } + }, editorState); public void ScheduleSwitchToExistingDifficulty(BeatmapInfo beatmapInfo, EditorState editorState) => scheduleDifficultySwitch(() => beatmapManager.GetWorkingBeatmap(beatmapInfo), editorState); From afc48d86df1be79ca37d02cb3fdee604d7f7ecaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 19:50:02 +0100 Subject: [PATCH 411/587] Add failing test coverage for save after safeties addition --- .../Editing/TestSceneEditorBeatmapCreation.cs | 1 + osu.Game/Screens/Edit/Editor.cs | 13 +++++++++---- osu.Game/Tests/Visual/EditorTestScene.cs | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 31348c1e17..a14c9aded3 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -109,6 +109,7 @@ namespace osu.Game.Tests.Visual.Editing && set != null && set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID); }); + AddAssert("can save again", () => Editor.Save()); AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); AddUntilStep("wait for created", () => diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 64b23ccda8..78c5c862f7 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -378,12 +378,16 @@ namespace osu.Game.Screens.Edit Clipboard.Content.Value = state.ClipboardContent; }); - protected void Save() + /// + /// Saves the currently edited beatmap. + /// + /// Whether the save was successful. + protected bool Save() { if (!canSave) { notifications?.Post(new SimpleErrorNotification { Text = "Saving is not supported for this ruleset yet, sorry!" }); - return; + return false; } try @@ -395,12 +399,13 @@ namespace osu.Game.Screens.Edit { // can fail e.g. due to duplicated difficulty names. Logger.Error(ex, ex.Message); - return; + return false; } // no longer new after first user-triggered save. isNewBeatmap = false; updateLastSavedHash(); + return true; } protected override void Update() @@ -809,7 +814,7 @@ namespace osu.Game.Screens.Edit { var fileMenuItems = new List { - new EditorMenuItem("Save", MenuItemType.Standard, Save) + new EditorMenuItem("Save", MenuItemType.Standard, () => Save()) }; if (RuntimeInfo.IsDesktop) diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 52d61ed422..da13c6862f 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual public new void Redo() => base.Redo(); - public new void Save() => base.Save(); + public new bool Save() => base.Save(); public new void Cut() => base.Cut(); From 47429fb0c68b6df09b73056a8a7a5b8d3fe2e4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 19:54:57 +0100 Subject: [PATCH 412/587] Fix same-name safety firing wrongly --- osu.Game/Beatmaps/BeatmapModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 327e10e643..d4df4b7870 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps string targetFilename = getFilename(beatmapInfo); // ensure that two difficulties from the set don't point at the same beatmap file. - if (setInfo.Beatmaps.Any(b => string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase))) + if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase))) throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'."); if (existingFileInfo != null) From a8ffc4fc2a08b27af6e295ee97cfee3d208d4bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jan 2022 19:58:21 +0100 Subject: [PATCH 413/587] Add editor override to respect `IsolateSavingFromDatabase` --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Tests/Visual/EditorTestScene.cs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d9b0ec3170..9c9b995955 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -112,7 +112,7 @@ namespace osu.Game.Beatmaps /// The new difficulty will be backed by a model /// and represented by the returned . /// - public WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + public virtual WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) { // fetch one of the existing difficulties to copy timing points and metadata from, // so that the user doesn't have to fill all of that out again. diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index da13c6862f..331bf04644 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -136,6 +136,12 @@ namespace osu.Game.Tests.Visual return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); } + public override WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + { + // don't actually care about properly creating a difficulty for this context. + return TestBeatmap; + } + private class TestWorkingBeatmapCache : WorkingBeatmapCache { private readonly TestBeatmapManager testBeatmapManager; From 62537eb4aa5f458fd959846a8bf318b16e2acef0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Feb 2022 12:44:33 +0900 Subject: [PATCH 414/587] Fix spectator not completing --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 21 +++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 0b2a7cecf6..460352ee07 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -112,6 +112,7 @@ namespace osu.Game.Screens.Spectate switch (e.Action) { case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Replace: foreach ((int userId, var state) in e.NewItems.AsNonNull()) onUserStateChanged(userId, state); break; @@ -120,11 +121,6 @@ namespace osu.Game.Screens.Spectate foreach ((int userId, _) in e.OldItems.AsNonNull()) onUserStateRemoved(userId); break; - - case NotifyDictionaryChangedAction.Replace: - foreach ((int userId, var state) in e.NewItems.AsNonNull()) - onUserStateChanged(userId, state); - break; } } @@ -136,11 +132,18 @@ namespace osu.Game.Screens.Spectate if (!userMap.ContainsKey(userId)) return; - // Do nothing for failed/completed states. - if (newState.State == SpectatingUserState.Playing) + switch (newState.State) { - Schedule(() => OnUserStateChanged(userId, newState)); - updateGameplayState(userId); + case SpectatingUserState.Completed: + // Make sure that gameplay completes to the end. + if (gameplayStates.TryGetValue(userId, out var gameplayState)) + gameplayState.Score.Replay.HasReceivedAllFrames = true; + break; + + case SpectatingUserState.Playing: + Schedule(() => OnUserStateChanged(userId, newState)); + updateGameplayState(userId); + break; } } From aff36d4e163c96c9dd35ea280a1b630806b63d10 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Feb 2022 11:52:37 +0800 Subject: [PATCH 415/587] Refactor `populateStatistics` to avoid disposing --- .../Ranking/Statistics/StatisticsPanel.cs | 128 +++++++++--------- 1 file changed, 63 insertions(+), 65 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 08be1de652..cbb4e5089d 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -86,88 +86,86 @@ namespace osu.Game.Screens.Ranking.Statistics playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); }, loadCancellation.Token).ContinueWith(t => Schedule(() => { - FillFlowContainer rows; - Container container = new OsuScrollContainer(Direction.Vertical) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - Children = new[] - { - rows = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(30, 15) - } - } - }; - - bool panelIsEmpty = true; - bool panelIsComplete = true; bool hitEventsAvailable = newScore.HitEvents.Count != 0; + Container container; - foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) + var statisticRows = newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap); + + if (!hitEventsAvailable && statisticRows.SelectMany(r => r.Columns).All(c => c.RequiresHitEvents)) { - var columnsToDisplay = hitEventsAvailable - ? row.Columns - : row.Columns.Where(c => !c.RequiresHitEvents).ToArray(); - - if (columnsToDisplay.Length < row.Columns.Length) - panelIsComplete = false; - - if (columnsToDisplay.Any()) - panelIsEmpty = false; - else - continue; - - rows.Add(new GridContainer + container = new FillFlowContainer { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - columnsToDisplay?.Select(c => new StatisticContainer(c) + new MessagePlaceholder("Extended statistics are only available after watching a replay!"), + new ReplayDownloadButton(newScore) { + Scale = new Vector2(1.5f), Anchor = Anchor.Centre, Origin = Anchor.Centre, - }).Cast().ToArray() - }, - ColumnDimensions = Enumerable.Range(0, columnsToDisplay.Length) - .Select(i => columnsToDisplay[i].Dimension ?? new Dimension()).ToArray(), - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }); + }, + } + }; } - - if (!hitEventsAvailable) + else { - if (panelIsEmpty) + FillFlowContainer rows; + container = new OsuScrollContainer(Direction.Vertical) { - // Replace the scroll container with fill flow container to get the message centered. - rows.Dispose(); - container.Dispose(); - container = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + Children = new[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Vertical, - Children = new Drawable[] + rows = new FillFlowContainer { - new MessagePlaceholder("Extended statistics are only available after watching a replay!"), - new ReplayDownloadButton(newScore) + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(30, 15) + } + } + }; + + bool panelIsComplete = true; + + foreach (var row in statisticRows) + { + var columnsToDisplay = hitEventsAvailable + ? row.Columns + : row.Columns.Where(c => !c.RequiresHitEvents).ToArray(); + + if (columnsToDisplay.Length < row.Columns.Length) + panelIsComplete = false; + + if (columnsToDisplay.Length == 0) + continue; + + rows.Add(new GridContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] + { + columnsToDisplay?.Select(c => new StatisticContainer(c) { - Scale = new Vector2(1.5f), Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, - } - }; + }).Cast().ToArray() + }, + ColumnDimensions = Enumerable.Range(0, columnsToDisplay.Length) + .Select(i => columnsToDisplay[i].Dimension ?? new Dimension()).ToArray(), + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }); } - else if (!panelIsComplete) + + if (!hitEventsAvailable && !panelIsComplete) { rows.Add(new FillFlowContainer { From d9a43b4c4ce50d58ff4d12393c9b4f4c66bc602d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Feb 2022 13:16:54 +0900 Subject: [PATCH 416/587] Fix API requests not completing when offline --- osu.Game/Online/API/APIAccess.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 8d91548149..1d2abe0c7f 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -399,7 +399,10 @@ namespace osu.Game.Online.API lock (queue) { if (state.Value == APIState.Offline) + { + request.Fail(new WebException("Disconnected from server")); return; + } queue.Enqueue(request); } From 62fa915193ba65a386d647ba63b0e5d422d18c64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 13:58:55 +0900 Subject: [PATCH 417/587] Standardise exception messages for local-user-logged-out flows --- osu.Game/Online/API/APIAccess.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 1d2abe0c7f..a1b8e5bee7 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -400,7 +400,7 @@ namespace osu.Game.Online.API { if (state.Value == APIState.Offline) { - request.Fail(new WebException("Disconnected from server")); + failFromAPIOffline(request); return; } @@ -419,7 +419,7 @@ namespace osu.Game.Online.API if (failOldRequests) { foreach (var req in oldQueueRequests) - req.Fail(new WebException(@"Disconnected from server")); + failFromAPIOffline(req); } } } @@ -440,6 +440,13 @@ namespace osu.Game.Online.API flushQueue(); } + private void failFromAPIOffline(APIRequest req) + { + Debug.Assert(state.Value == APIState.Offline); + + req.Fail(new WebException(@"User not logged in")); + } + private static APIUser createGuestUser() => new GuestUser(); protected override void Dispose(bool isDisposing) From a69c7a9de6f80b3b95b628abc1b5711c6338743e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 14:09:27 +0900 Subject: [PATCH 418/587] Split exceptions back out to give better messaging --- osu.Game/Online/API/APIAccess.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index a1b8e5bee7..c5302a393c 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -400,7 +400,7 @@ namespace osu.Game.Online.API { if (state.Value == APIState.Offline) { - failFromAPIOffline(request); + request.Fail(new WebException(@"User not logged in")); return; } @@ -419,7 +419,7 @@ namespace osu.Game.Online.API if (failOldRequests) { foreach (var req in oldQueueRequests) - failFromAPIOffline(req); + req.Fail(new WebException($@"Request failed from flush operation (state {state.Value})")); } } } @@ -440,13 +440,6 @@ namespace osu.Game.Online.API flushQueue(); } - private void failFromAPIOffline(APIRequest req) - { - Debug.Assert(state.Value == APIState.Offline); - - req.Fail(new WebException(@"User not logged in")); - } - private static APIUser createGuestUser() => new GuestUser(); protected override void Dispose(bool isDisposing) From c8ce00b26a5c9ca1049274cd7eee7e0c2f54e075 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 14:50:40 +0900 Subject: [PATCH 419/587] Trigger a re-layout of HUD components when scoring mode is changed This is a simple way of fixing the layout of scoring elements overlapping due to different score display width requirements of different scoring modes. It will only resolve the case where a user hasn't customsied the layout of the default skins, but as this is a very simple / low effort implementation for the most common scenario, I think it makes sense. Closes https://github.com/ppy/osu/issues/16067. --- osu.Game/Screens/Play/HUDOverlay.cs | 30 +++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index fdb5d418f3..628452fbc8 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -17,6 +17,7 @@ using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; @@ -83,10 +84,7 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { CreateFailingLayer(), - mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents) - { - RelativeSizeAxes = Axes.Both, - }, + mainComponents = new MainComponentsContainer(), topRightElements = new FillFlowContainer { Anchor = Anchor.TopRight, @@ -325,5 +323,29 @@ namespace osu.Game.Screens.Play break; } } + + private class MainComponentsContainer : SkinnableTargetContainer + { + private Bindable scoringMode; + + [Resolved] + private OsuConfigManager config { get; set; } + + public MainComponentsContainer() + : base(SkinnableTarget.MainHUDComponents) + { + RelativeSizeAxes = Axes.Both; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // When the scoring mode changes, relative positions of elements may change (see DefaultSkin.GetDrawableComponent). + // This is a best effort implementation for cases where users haven't customised layouts. + scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + scoringMode.BindValueChanged(val => Reload()); + } + } } } From 6355ac66634834124adfb30be16a3a3d138ada9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 15:10:06 +0900 Subject: [PATCH 420/587] Wait for `DialogOverlay` load in more tests Apparently the previous fix was not enough as this can still be seen failing (https://github.com/ppy/osu/runs/5046718623?check_suite_focus=true). This change is copying from what other tests use seemingly reliably, such as `TestScenePerformFromScreen`) --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index ed484e03f6..e31377b96e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -128,6 +128,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("choose clear all scores", () => InputManager.Key(Key.Number4)); + AddUntilStep("wait for dialog display", () => Game.Dependencies.Get().IsLoaded); AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null); AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null); @@ -172,6 +173,7 @@ namespace osu.Game.Tests.Visual.Navigation InputManager.Click(MouseButton.Left); }); + AddUntilStep("wait for dialog display", () => Game.Dependencies.Get().IsLoaded); AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null); AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null); From 41aa4b8cca63be85c7a8434618c0dc3189f04f87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 16:04:05 +0900 Subject: [PATCH 421/587] Fix `TestSelectingFilteredRuleset` failing under visual tests due to using local database --- .../SongSelect/TestSceneBeatmapCarousel.cs | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index a23bc620ec..4e46901e08 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -601,7 +601,7 @@ namespace osu.Game.Tests.Visual.SongSelect { BeatmapSetInfo testMixed = null; - createCarousel(); + createCarousel(new List()); AddStep("add mixed ruleset beatmapset", () => { @@ -765,22 +765,22 @@ namespace osu.Game.Tests.Visual.SongSelect { bool changed = false; - createCarousel(c => + if (beatmapSets == null) + { + beatmapSets = new List(); + + for (int i = 1; i <= (count ?? set_count); i++) + { + beatmapSets.Add(randomDifficulties + ? TestResources.CreateTestBeatmapSetInfo() + : TestResources.CreateTestBeatmapSetInfo(3)); + } + } + + createCarousel(beatmapSets, c => { carouselAdjust?.Invoke(c); - if (beatmapSets == null) - { - beatmapSets = new List(); - - for (int i = 1; i <= (count ?? set_count); i++) - { - beatmapSets.Add(randomDifficulties - ? TestResources.CreateTestBeatmapSetInfo() - : TestResources.CreateTestBeatmapSetInfo(3)); - } - } - carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria()); carousel.BeatmapSetsChanged = () => changed = true; carousel.BeatmapSets = beatmapSets; @@ -789,7 +789,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Wait for load", () => changed); } - private void createCarousel(Action carouselAdjust = null, Container target = null) + private void createCarousel(List beatmapSets, Action carouselAdjust = null, Container target = null) { AddStep("Create carousel", () => { @@ -803,6 +803,8 @@ namespace osu.Game.Tests.Visual.SongSelect carouselAdjust?.Invoke(carousel); + carousel.BeatmapSets = beatmapSets; + (target ?? this).Child = carousel; }); } From e65996efc31b5c17d7386baa1548d204889849ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 17:14:38 +0900 Subject: [PATCH 422/587] Rename variable to match purpose better --- osu.Game/Screens/Select/SongSelect.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 44af63a554..ee807762bf 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -110,7 +110,7 @@ namespace osu.Game.Screens.Select private double audioFeedbackLastPlaybackTime; - private bool randomClicked; + private bool randomSelectionPending; [Resolved] private MusicController music { get; set; } @@ -321,12 +321,12 @@ namespace osu.Game.Screens.Select { NextRandom = () => { - randomClicked = true; + randomSelectionPending = true; Carousel.SelectNextRandom(); }, PreviousRandom = () => { - randomClicked = true; + randomSelectionPending = true; Carousel.SelectPreviousRandom(); } }, null), @@ -498,7 +498,7 @@ namespace osu.Game.Screens.Select { if (beatmap != null && beatmapInfoPrevious != null && Time.Current - audioFeedbackLastPlaybackTime >= 50) { - if (randomClicked) + if (randomSelectionPending) sampleRandomBeatmap.Play(); else if (beatmap.BeatmapSet?.ID == beatmapInfoPrevious.BeatmapSet?.ID) sampleChangeDifficulty.Play(); @@ -508,7 +508,7 @@ namespace osu.Game.Screens.Select audioFeedbackLastPlaybackTime = Time.Current; } - randomClicked = false; + randomSelectionPending = false; beatmapInfoPrevious = beatmap; } From a27d0572ed3d5c8b4c5f082015de2f6c33b86005 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Feb 2022 17:00:40 +0800 Subject: [PATCH 423/587] Add test cases for manual testing --- .../Ranking/TestSceneStatisticsPanel.cs | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index f64b7b2b65..35281a85eb 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -6,10 +6,18 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Tests.Resources; using osuTK; @@ -41,6 +49,24 @@ namespace osu.Game.Tests.Visual.Ranking loadPanel(TestResources.CreateTestScoreInfo()); } + [Test] + public void TestScoreInRulesetWhereAllStatsRequireHitEvents() + { + loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetAllStatsRequireHitEvents().RulesetInfo)); + } + + [Test] + public void TestScoreInRulesetWhereNoStatsRequireHitEvents() + { + loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetNoStatsRequireHitEvents().RulesetInfo)); + } + + [Test] + public void TestScoreInMixedRuleset() + { + loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetMixed().RulesetInfo)); + } + [Test] public void TestNullScore() { @@ -75,5 +101,134 @@ namespace osu.Game.Tests.Visual.Ranking return hitEvents; } + + private class TestRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) + { + throw new NotImplementedException(); + } + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) + { + throw new NotImplementedException(); + } + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) + { + throw new NotImplementedException(); + } + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) + { + throw new NotImplementedException(); + } + + public override string Description => string.Empty; + + public override string ShortName => string.Empty; + + protected static Drawable CreatePlaceholderStatistic(string message) => new Container + { + RelativeSizeAxes = Axes.X, + Masking = true, + CornerRadius = 20, + Height = 250, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(0.5f), + Alpha = 0.5f + }, + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Text = message, + Margin = new MarginPadding { Left = 20 } + } + } + }; + } + + private class TestRulesetAllStatsRequireHitEvents : TestRuleset + { + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Requiring Hit Events 1", + () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true) + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Requiring Hit Events 2", + () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true) + } + } + }; + } + } + + private class TestRulesetNoStatsRequireHitEvents : TestRuleset + { + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Not Requiring Hit Events 1", + () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")) + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Not Requiring Hit Events 2", + () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")) + } + } + }; + } + } + + private class TestRulesetMixed : TestRuleset + { + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Requiring Hit Events", + () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true) + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Not Requiring Hit Events", + () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")) + } + } + }; + } + } } } From 6d6327d3da550900a717b166a8220f09ab1dde16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 18:40:10 +0900 Subject: [PATCH 424/587] Fix test beatmap loading potentially performing selection before carousel itself is loaded --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 0f5c708ab8..f17daa8697 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -108,7 +108,7 @@ namespace osu.Game.Screens.Select set { loadedTestBeatmaps = true; - loadBeatmapSets(value); + Schedule(() => loadBeatmapSets(value)); } } From 6974c2d255f400be9401479df9e782c79a2c04b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 19:00:03 +0900 Subject: [PATCH 425/587] Remove weird `panelIsComplete` flag and replace LINQ with simple `foreach` --- .../Ranking/Statistics/StatisticsPanel.cs | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index cbb4e5089d..898bd69b2c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -131,41 +132,48 @@ namespace osu.Game.Screens.Ranking.Statistics } }; - bool panelIsComplete = true; + bool anyRequiredHitEvents = false; foreach (var row in statisticRows) { - var columnsToDisplay = hitEventsAvailable - ? row.Columns - : row.Columns.Where(c => !c.RequiresHitEvents).ToArray(); + var columns = row.Columns; - if (columnsToDisplay.Length < row.Columns.Length) - panelIsComplete = false; - - if (columnsToDisplay.Length == 0) + if (columns.Length == 0) continue; + var columnContent = new List(); + var dimensions = new List(); + + foreach (var col in columns) + { + if (!hitEventsAvailable && col.RequiresHitEvents) + { + anyRequiredHitEvents = true; + continue; + } + + columnContent.Add(new StatisticContainer(col) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + dimensions.Add(col.Dimension ?? new Dimension()); + } + rows.Add(new GridContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Content = new[] - { - columnsToDisplay?.Select(c => new StatisticContainer(c) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }).Cast().ToArray() - }, - ColumnDimensions = Enumerable.Range(0, columnsToDisplay.Length) - .Select(i => columnsToDisplay[i].Dimension ?? new Dimension()).ToArray(), + Content = new[] { columnContent.ToArray() }, + ColumnDimensions = dimensions.ToArray(), RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } - if (!hitEventsAvailable && !panelIsComplete) + if (anyRequiredHitEvents) { rows.Add(new FillFlowContainer { From 47d577ec9c822edb762c91067fb7c17ea97a766a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 19:17:56 +0900 Subject: [PATCH 426/587] Add back constructor for ruleset compatibility --- osu.Game/Screens/Ranking/Statistics/StatisticItem.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index 3cfc38e263..b43fbbdeee 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -33,6 +33,12 @@ namespace osu.Game.Screens.Ranking.Statistics /// public readonly bool RequiresHitEvents; + [Obsolete("Use constructor which takes creation function instead.")] // Can be removed 20220803. + public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) + : this(name, () => content, true, dimension) + { + } + /// /// Creates a new , to be displayed inside a in the results screen. /// From ad47649d1c5044c64c4a4f8d50ff739031019221 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 19:38:53 +0900 Subject: [PATCH 427/587] Make `BeatmapModelManager.Save` non-virtual --- osu.Game/Beatmaps/BeatmapModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index d4df4b7870..a9da221e08 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -46,7 +46,7 @@ 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 beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) + public void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) { var setInfo = beatmapInfo.BeatmapSet; Debug.Assert(setInfo != null); From bef0a2da210c7505adef18314ec12312b3ee59d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 19:39:24 +0900 Subject: [PATCH 428/587] Remove return type from `AddDifficultyToBeatmapSet` Also removes a pointless realm encapsulation. --- osu.Game/Beatmaps/BeatmapManager.cs | 6 +++--- osu.Game/Beatmaps/BeatmapModelManager.cs | 15 +++++---------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 9c9b995955..d60cfdee9e 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -128,10 +128,10 @@ namespace osu.Game.Beatmaps foreach (var timingPoint in referenceBeatmap.Beatmap.ControlPointInfo.TimingPoints) newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); - var createdBeatmapInfo = beatmapModelManager.AddDifficultyToBeatmapSet(beatmapSetInfo, newBeatmap); + beatmapModelManager.AddDifficultyToBeatmapSet(beatmapSetInfo, newBeatmap); - workingBeatmapCache.Invalidate(createdBeatmapInfo.BeatmapSet); - return GetWorkingBeatmap(createdBeatmapInfo); + workingBeatmapCache.Invalidate(beatmapSetInfo); + return GetWorkingBeatmap(newBeatmap.BeatmapInfo); } // TODO: add back support for making a copy of another difficulty diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index a9da221e08..b9f0af8833 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -93,19 +93,14 @@ namespace osu.Game.Beatmaps /// /// Add a new difficulty to the beatmap set represented by the provided . /// - public BeatmapInfo AddDifficultyToBeatmapSet(BeatmapSetInfo beatmapSetInfo, Beatmap beatmap) + public void AddDifficultyToBeatmapSet(BeatmapSetInfo beatmapSetInfo, Beatmap beatmap) { - return Realm.Run(realm => - { - var beatmapInfo = beatmap.BeatmapInfo; + var beatmapInfo = beatmap.BeatmapInfo; - beatmapSetInfo.Beatmaps.Add(beatmapInfo); - beatmapInfo.BeatmapSet = beatmapSetInfo; + beatmapSetInfo.Beatmaps.Add(beatmapInfo); + beatmapInfo.BeatmapSet = beatmapSetInfo; - Save(beatmapInfo, beatmap); - - return beatmapInfo.Detach(); - }); + Save(beatmapInfo, beatmap); } private static string getFilename(BeatmapInfo beatmapInfo) From 40953751b542281dca26e24c323e48b73b2a6c60 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Thu, 3 Feb 2022 13:28:49 +0100 Subject: [PATCH 429/587] Use `ScreenOrientation.FullUser` on Android tablets --- osu.Android/GameplayScreenRotationLocker.cs | 2 +- osu.Android/OsuGameActivity.cs | 25 ++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index ab91b4e3b3..1b7893e5b9 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -27,7 +27,7 @@ namespace osu.Android { gameActivity.RunOnUiThread(() => { - gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : OsuGameActivity.DEFAULT_ORIENTATION; + gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.ScreenOrientation; }); } } diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index e6679b61a6..7de597fe88 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.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.IO; using System.Linq; @@ -8,16 +9,18 @@ using System.Threading.Tasks; using Android.App; using Android.Content; using Android.Content.PM; -using Android.Net; +using Android.Graphics; using Android.OS; using Android.Provider; using Android.Views; using osu.Framework.Android; using osu.Game.Database; +using Debug = System.Diagnostics.Debug; +using Uri = Android.Net.Uri; namespace osu.Android { - [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true, ScreenOrientation = DEFAULT_ORIENTATION)] + [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")] @@ -39,10 +42,10 @@ namespace osu.Android [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity { - public const ScreenOrientation DEFAULT_ORIENTATION = ScreenOrientation.SensorLandscape; - private static readonly string[] osu_url_schemes = { "osu", "osump" }; + public ScreenOrientation ScreenOrientation = ScreenOrientation.Unspecified; + private OsuGameAndroid game; protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); @@ -56,8 +59,20 @@ namespace osu.Android // reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent) handleIntent(Intent); + Debug.Assert(Window != null); + Window.AddFlags(WindowManagerFlags.Fullscreen); Window.AddFlags(WindowManagerFlags.KeepScreenOn); + + Debug.Assert(WindowManager?.DefaultDisplay != null); + Debug.Assert(Resources?.DisplayMetrics != null); + + Point displaySize = new Point(); + WindowManager.DefaultDisplay.GetSize(displaySize); + float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density; + bool isTablet = smallestWidthDp >= 600f; + + RequestedOrientation = ScreenOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; } protected override void OnNewIntent(Intent intent) => handleIntent(intent); @@ -106,7 +121,7 @@ namespace osu.Android cursor.MoveToFirst(); - var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); + int filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); string filename = cursor.GetString(filenameColumn); // SharpCompress requires archive streams to be seekable, which the stream opened by From f2850601486ec6a8b4b4a88d76b13c75fddd0320 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Feb 2022 21:50:15 +0900 Subject: [PATCH 430/587] Fix MultiSpectatorScreen not continuing to results --- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 5 ++++- osu.Game/Screens/Play/SoloSpectator.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 16 ++++++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 4646f42d63..bbdd7a3d56 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -215,8 +215,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score); - protected override void EndGameplay(int userId) + protected override void EndGameplay(int userId, SpectatorState state) { + if (state.State == SpectatingUserState.Completed || state.State == SpectatingUserState.Failed) + return; + RemoveUser(userId); var instance = instances.Single(i => i.UserId == userId); diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index b530965269..a710db6d24 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -180,7 +180,7 @@ namespace osu.Game.Screens.Play scheduleStart(spectatorGameplayState); } - protected override void EndGameplay(int userId) + protected override void EndGameplay(int userId, SpectatorState state) { scheduledStart?.Cancel(); immediateSpectatorGameplayState = null; diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 460352ee07..7b58f669a0 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -118,8 +118,8 @@ namespace osu.Game.Screens.Spectate break; case NotifyDictionaryChangedAction.Remove: - foreach ((int userId, _) in e.OldItems.AsNonNull()) - onUserStateRemoved(userId); + foreach ((int userId, SpectatorState state) in e.OldItems.AsNonNull()) + onUserStateRemoved(userId, state); break; } } @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Spectate } } - private void onUserStateRemoved(int userId) + private void onUserStateRemoved(int userId, SpectatorState state) { if (!userMap.ContainsKey(userId)) return; @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Spectate gameplayState.Score.Replay.HasReceivedAllFrames = true; gameplayStates.Remove(userId); - Schedule(() => EndGameplay(userId)); + Schedule(() => EndGameplay(userId, state)); } private void updateGameplayState(int userId) @@ -212,7 +212,8 @@ namespace osu.Game.Screens.Spectate /// Ends gameplay for a user. /// /// The user to end gameplay for. - protected abstract void EndGameplay(int userId); + /// The final user state. + protected abstract void EndGameplay(int userId, SpectatorState state); /// /// Stops spectating a user. @@ -220,7 +221,10 @@ namespace osu.Game.Screens.Spectate /// The user to stop spectating. protected void RemoveUser(int userId) { - onUserStateRemoved(userId); + if (!userStates.TryGetValue(userId, out var state)) + return; + + onUserStateRemoved(userId, state); users.Remove(userId); userMap.Remove(userId); From 84171962e56faa9b5310450d01d0faedeffd2146 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Thu, 3 Feb 2022 13:55:04 +0100 Subject: [PATCH 431/587] Change name and add xmldoc --- osu.Android/GameplayScreenRotationLocker.cs | 2 +- osu.Android/OsuGameActivity.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index 1b7893e5b9..2e83f784d3 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -27,7 +27,7 @@ namespace osu.Android { gameActivity.RunOnUiThread(() => { - gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.ScreenOrientation; + gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.DefaultOrientation; }); } } diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 7de597fe88..eebd079f68 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -44,7 +44,11 @@ namespace osu.Android { private static readonly string[] osu_url_schemes = { "osu", "osump" }; - public ScreenOrientation ScreenOrientation = ScreenOrientation.Unspecified; + /// + /// The default screen orientation. + /// + /// Adjusted on startup to match expected UX for the current device type (phone/tablet). + public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; private OsuGameAndroid game; @@ -72,7 +76,7 @@ namespace osu.Android float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density; bool isTablet = smallestWidthDp >= 600f; - RequestedOrientation = ScreenOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; + RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; } protected override void OnNewIntent(Intent intent) => handleIntent(intent); From d4ebff6ea1f381bad406c692ab15b097fbdefd2b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Feb 2022 23:18:22 +0900 Subject: [PATCH 432/587] Add failing test --- .../Multiplayer/TestSceneMultiplayer.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 3563869d8b..27bf1c209c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -871,6 +871,52 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0); } + [Test] + public void TestGameplayStartsWhileInSpectatorScreen() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddStep("join other user and make host", () => + { + client.AddUser(new APIUser { Id = 1234 }); + client.TransferHost(1234); + }); + + AddStep("set local user spectating", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("wait for spectating state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + + runGameplay(); + + AddStep("exit gameplay for other user", () => client.ChangeUserState(1234, MultiplayerUserState.Idle)); + AddUntilStep("wait for room to be idle", () => client.Room?.State == MultiplayerRoomState.Open); + + runGameplay(); + + void runGameplay() + { + AddStep("start match by other user", () => + { + client.ChangeUserState(1234, MultiplayerUserState.Ready); + client.StartMatch(); + }); + + AddUntilStep("wait for loading", () => client.Room?.State == MultiplayerRoomState.WaitingForLoad); + AddStep("set player loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); + AddUntilStep("wait for gameplay to start", () => client.Room?.State == MultiplayerRoomState.Playing); + } + } + private void enterGameplay() { pressReadyButton(); From 6dc0f3fd960cc5a4fd914123388a0585901e0243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Feb 2022 18:14:30 +0100 Subject: [PATCH 433/587] Merge difficulty creation methods into one One of them wasn't really doing much anymore and was more obfuscating what was actually happening at this point. --- osu.Game/Beatmaps/BeatmapManager.cs | 14 +++++++++----- osu.Game/Beatmaps/BeatmapModelManager.cs | 13 ------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d60cfdee9e..633eb8f15e 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -120,15 +120,19 @@ namespace osu.Game.Beatmaps // but cases where this isn't true seem rather rare / pathological. var referenceBeatmap = GetWorkingBeatmap(beatmapSetInfo.Beatmaps.First()); - var newBeatmap = new Beatmap - { - BeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()) - }; + var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); + // populate circular beatmap set info <-> beatmap info references manually. + // several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()` + // rely on them being freely traversable in both directions for correct operation. + beatmapSetInfo.Beatmaps.Add(newBeatmapInfo); + newBeatmapInfo.BeatmapSet = beatmapSetInfo; + + var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; foreach (var timingPoint in referenceBeatmap.Beatmap.ControlPointInfo.TimingPoints) newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); - beatmapModelManager.AddDifficultyToBeatmapSet(beatmapSetInfo, newBeatmap); + beatmapModelManager.Save(newBeatmapInfo, newBeatmap); workingBeatmapCache.Invalidate(beatmapSetInfo); return GetWorkingBeatmap(newBeatmap.BeatmapInfo); diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index b9f0af8833..4c680bbcc9 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -90,19 +90,6 @@ namespace osu.Game.Beatmaps WorkingBeatmapCache?.Invalidate(beatmapInfo); } - /// - /// Add a new difficulty to the beatmap set represented by the provided . - /// - public void AddDifficultyToBeatmapSet(BeatmapSetInfo beatmapSetInfo, Beatmap beatmap) - { - var beatmapInfo = beatmap.BeatmapInfo; - - beatmapSetInfo.Beatmaps.Add(beatmapInfo); - beatmapInfo.BeatmapSet = beatmapSetInfo; - - Save(beatmapInfo, beatmap); - } - private static string getFilename(BeatmapInfo beatmapInfo) { var metadata = beatmapInfo.Metadata; From ee1feae8062919f2992bd41b23cabd19aa49f345 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 11:06:18 +0900 Subject: [PATCH 434/587] Remove unnecessary ruleset ordering Co-authored-by: Salman Ahmed --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 78c5c862f7..5503a62ba2 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -834,7 +834,7 @@ namespace osu.Game.Screens.Edit { var rulesetItems = new List(); - foreach (var ruleset in rulesets.AvailableRulesets.OrderBy(ruleset => ruleset.OnlineID)) + foreach (var ruleset in rulesets.AvailableRulesets) rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => CreateNewDifficulty(ruleset))); return new EditorMenuItem("Create new difficulty") { Items = rulesetItems }; From bbef12e72c7bbaaf41c9a9b34b9d90ce5b4cfc2e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Feb 2022 05:45:12 +0300 Subject: [PATCH 435/587] Refactor `ExpandingControlContainer` to no longer rely on controls --- ...iner.cs => TestSceneExpandingContainer.cs} | 40 ++---------- osu.Game/Overlays/ExpandableSlider.cs | 4 +- osu.Game/Overlays/ExpandingButtonContainer.cs | 6 +- ...trolContainer.cs => ExpandingContainer.cs} | 64 ++++++------------- osu.Game/Overlays/IExpandable.cs | 11 ---- osu.Game/Overlays/IExpandableControl.cs | 12 ---- 6 files changed, 28 insertions(+), 109 deletions(-) rename osu.Game.Tests/Visual/UserInterface/{TestSceneExpandingControlContainer.cs => TestSceneExpandingContainer.cs} (74%) rename osu.Game/Overlays/{ExpandingControlContainer.cs => ExpandingContainer.cs} (54%) delete mode 100644 osu.Game/Overlays/IExpandableControl.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs similarity index 74% rename from osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs index 48089566cc..f63591311f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs @@ -1,20 +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.Linq; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections; using osuTK; -using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneExpandingControlContainer : OsuManualInputManagerTestScene + public class TestSceneExpandingContainer : OsuManualInputManagerTestScene { private TestExpandingContainer container; private SettingsToolboxGroup toolboxGroup; @@ -84,44 +80,22 @@ namespace osu.Game.Tests.Visual.UserInterface } /// - /// Tests hovering over controls expands the parenting container appropriately and does not contract until hover is lost from container. + /// Tests hovering expands the container and does not contract until hover is lost. /// [Test] - public void TestHoveringControlExpandsContainer() + public void TestHoveringExpandsContainer() { AddAssert("ensure container contracted", () => !container.Expanded.Value); - AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); + AddStep("hover container", () => InputManager.MoveMouseTo(container)); AddAssert("container expanded", () => container.Expanded.Value); AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); - AddStep("hover group top", () => InputManager.MoveMouseTo(toolboxGroup.ScreenSpaceDrawQuad.TopLeft + new Vector2(5))); - AddAssert("container still expanded", () => container.Expanded.Value); - AddAssert("controls still expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); - AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); AddAssert("container contracted", () => !container.Expanded.Value); AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); } - /// - /// Tests dragging a UI control (e.g. ) outside its parenting container does not contract it until dragging is finished. - /// - [Test] - public void TestDraggingControlOutsideDoesntContractContainer() - { - AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); - AddAssert("container expanded", () => container.Expanded.Value); - - AddStep("hover slider nub", () => InputManager.MoveMouseTo(slider1.ChildrenOfType().Single())); - AddStep("hold slider nub", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag outside container", () => InputManager.MoveMouseTo(Vector2.Zero)); - AddAssert("container still expanded", () => container.Expanded.Value); - - AddStep("release slider nub", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("container contracted", () => !container.Expanded.Value); - } - /// /// Tests expanding a container will expand underlying groups if contracted. /// @@ -157,21 +131,21 @@ namespace osu.Game.Tests.Visual.UserInterface } /// - /// Tests expanding a container via does not get contracted by losing hover. + /// Tests expanding a container via does not get contracted by losing hover. /// [Test] public void TestExpandingContainerDoesntGetContractedByHover() { AddStep("expand container", () => container.Expanded.Value = true); - AddStep("hover control", () => InputManager.MoveMouseTo(slider1)); + AddStep("hover container", () => InputManager.MoveMouseTo(container)); AddAssert("container still expanded", () => container.Expanded.Value); AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); AddAssert("container still expanded", () => container.Expanded.Value); } - private class TestExpandingContainer : ExpandingControlContainer + private class TestExpandingContainer : ExpandingContainer { public TestExpandingContainer() : base(120, 250) diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs index d346b9b22c..062de98659 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays /// /// An implementation for the UI slider bar control. /// - public class ExpandableSlider : CompositeDrawable, IExpandableControl, IHasCurrentValue + public class ExpandableSlider : CompositeDrawable, IExpandable, IHasCurrentValue where T : struct, IEquatable, IComparable, IConvertible where TSlider : OsuSliderBar, new() { @@ -72,8 +72,6 @@ namespace osu.Game.Overlays public BindableBool Expanded { get; } = new BindableBool(); - bool IExpandable.ShouldBeExpanded => IsHovered || slider.IsDragged; - public override bool HandlePositionalInput => true; public ExpandableSlider() diff --git a/osu.Game/Overlays/ExpandingButtonContainer.cs b/osu.Game/Overlays/ExpandingButtonContainer.cs index d7ff285707..8fb3e1b550 100644 --- a/osu.Game/Overlays/ExpandingButtonContainer.cs +++ b/osu.Game/Overlays/ExpandingButtonContainer.cs @@ -1,17 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Graphics.UserInterface; - namespace osu.Game.Overlays { /// - /// An with a long hover expansion delay for buttons. + /// An with a long hover expansion delay. /// /// /// Mostly used for buttons with explanatory labels, in which the label would display after a "long hover". /// - public class ExpandingButtonContainer : ExpandingControlContainer + public class ExpandingButtonContainer : ExpandingContainer { protected ExpandingButtonContainer(float contractedWidth, float expandedWidth) : base(contractedWidth, expandedWidth) diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingContainer.cs similarity index 54% rename from osu.Game/Overlays/ExpandingControlContainer.cs rename to osu.Game/Overlays/ExpandingContainer.cs index 859e4bcd25..ea3fffcb78 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingContainer.cs @@ -1,23 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Containers; namespace osu.Game.Overlays { /// - /// Represents a with the ability to expand/contract when hovering the controls within it. + /// Represents a with the ability to expand/contract on hover. /// - /// The type of UI control to lookup for hover expansion. - public class ExpandingControlContainer : Container, IExpandingContainer - where TControl : class, IDrawable + public class ExpandingContainer : Container, IExpandingContainer { private readonly float contractedWidth; private readonly float expandedWidth; @@ -33,7 +29,7 @@ namespace osu.Game.Overlays protected FillFlowContainer FillFlow { get; } - protected ExpandingControlContainer(float contractedWidth, float expandedWidth) + protected ExpandingContainer(float contractedWidth, float expandedWidth) { this.contractedWidth = contractedWidth; this.expandedWidth = expandedWidth; @@ -57,7 +53,6 @@ namespace osu.Game.Overlays } private ScheduledDelegate hoverExpandEvent; - private TControl activeControl; protected override void LoadComplete() { @@ -69,61 +64,38 @@ namespace osu.Game.Overlays }, true); } - protected override void Update() - { - base.Update(); - - // if the container was expanded from hovering over a control, we have to check per-frame whether we can contract it back. - // that's because contracting the container depends not only on whether it's no longer hovered, - // but also on whether the hovered control is no longer in a dragged state (if it was). - if (hoverExpandEvent != null && !IsHovered && (activeControl == null || !isControlActive(activeControl))) - { - hoverExpandEvent?.Cancel(); - - Expanded.Value = false; - hoverExpandEvent = null; - activeControl = null; - } - } - protected override bool OnHover(HoverEvent e) { - queueExpandIfHovering(); + updateHoverExpansion(); return true; } protected override bool OnMouseMove(MouseMoveEvent e) { - queueExpandIfHovering(); + updateHoverExpansion(); return base.OnMouseMove(e); } - private void queueExpandIfHovering() + protected override void OnHoverLost(HoverLostEvent e) { - // if the same control is hovered or dragged, let the scheduled expand play out.. - if (activeControl != null && isControlActive(activeControl)) + if (hoverExpandEvent != null) + { + hoverExpandEvent?.Cancel(); + hoverExpandEvent = null; + + Expanded.Value = false; return; + } - // ..otherwise check whether a new control is hovered, and if so, queue a new hover operation. - hoverExpandEvent?.Cancel(); - - // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way - // to handle cases like the editor where the controls may be nested within a child hierarchy. - activeControl = FillFlow.ChildrenOfType().FirstOrDefault(isControlActive); - - if (activeControl != null && !Expanded.Value) - hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); + base.OnHoverLost(e); } - /// - /// Whether the given control is currently active, by checking whether it's hovered or dragged. - /// - private bool isControlActive(TControl control) + private void updateHoverExpansion() { - if (control is IExpandable expandable) - return expandable.ShouldBeExpanded; + hoverExpandEvent?.Cancel(); - return control.IsHovered || control.IsDragged; + if (IsHovered && !Expanded.Value) + hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); } } } diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Overlays/IExpandable.cs index f998fc7b9f..770ac97847 100644 --- a/osu.Game/Overlays/IExpandable.cs +++ b/osu.Game/Overlays/IExpandable.cs @@ -3,7 +3,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays { @@ -16,15 +15,5 @@ namespace osu.Game.Overlays /// Whether this drawable is in an expanded state. /// BindableBool Expanded { get; } - - /// - /// Whether this drawable should be/stay expanded by a parenting . - /// By default, this is when this drawable is in a hovered or dragged state. - /// - /// - /// This is defined for certain controls which may have a child handling dragging instead. - /// (e.g. in which dragging is handled by their underlying control). - /// - bool ShouldBeExpanded => IsHovered || IsDragged; } } diff --git a/osu.Game/Overlays/IExpandableControl.cs b/osu.Game/Overlays/IExpandableControl.cs deleted file mode 100644 index 2cda6f467b..0000000000 --- a/osu.Game/Overlays/IExpandableControl.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Overlays -{ - /// - /// An interface for UI controls with the ability to expand/contract. - /// - public interface IExpandableControl : IExpandable - { - } -} From 4728919bcaaa7151f3d3036a629212f8e395f570 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 15:45:27 +0900 Subject: [PATCH 436/587] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index f89994cd56..04c543750e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 50cef71b26..83c3593edb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 9ec0f1c0a0..b0c056ea21 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From 52fdf0349f32a73ad1022a9b5ef26de9c39edc36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 16:07:05 +0900 Subject: [PATCH 437/587] Add safe area support via `ScalingContainer` --- .../Graphics/Containers/ScalingContainer.cs | 10 +++++++- osu.Game/OsuGameBase.cs | 24 ++++++++++++------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index d2b1e5e523..f9bd571131 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -101,6 +101,9 @@ namespace osu.Game.Graphics.Containers } } + [Resolved] + private ISafeArea safeArea { get; set; } + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { @@ -118,6 +121,8 @@ namespace osu.Game.Graphics.Containers posY = config.GetBindable(OsuSetting.ScalingPositionY); posY.ValueChanged += _ => updateSize(); + + safeArea.SafeAreaPadding.BindValueChanged(_ => updateSize()); } protected override void LoadComplete() @@ -161,7 +166,10 @@ namespace osu.Game.Graphics.Containers var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One; var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero; - bool requiresMasking = scaling && targetSize != Vector2.One; + bool requiresMasking = scaling && targetSize != Vector2.One + // For the top level scaling container, for now we apply masking if safe areas are in use. + // In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas. + || (targetMode == ScalingMode.Everything && safeArea.SafeAreaPadding.Value.Total != Vector2.Zero); if (requiresMasking) sizableContainer.Masking = true; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1713e73905..5f87abcfed 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -299,16 +299,22 @@ namespace osu.Game GlobalActionContainer globalBindings; - var mainContent = new Drawable[] + base.Content.Add(new SafeAreaContainer { - MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }, - // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. - globalBindings = new GlobalActionContainer(this) - }; - - MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }; - - base.Content.Add(CreateScalingContainer().WithChildren(mainContent)); + RelativeSizeAxes = Axes.Both, + Child = CreateScalingContainer().WithChildren(new Drawable[] + { + (MenuCursorContainer = new MenuCursorContainer + { + RelativeSizeAxes = Axes.Both + }).WithChild(content = new OsuTooltipContainer(MenuCursorContainer.Cursor) + { + RelativeSizeAxes = Axes.Both + }), + // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. + globalBindings = new GlobalActionContainer(this) + }) + }); KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); From 1444df4d50ce0e2d9417f0958d4a39f76ae8e85e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 16:37:11 +0900 Subject: [PATCH 438/587] Add test scene for playing with safe areas --- .../TestSceneSafeAreaHandling.cs | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs new file mode 100644 index 0000000000..676ae1276b --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs @@ -0,0 +1,116 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays.Settings; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneSafeAreaHandling : OsuGameTestScene + { + private SafeAreaDefiningContainer safeAreaContainer; + + private static BindableSafeArea safeArea; + + private readonly Bindable safeAreaPaddingTop = new BindableFloat { MinValue = 0, MaxValue = 200 }; + private readonly Bindable safeAreaPaddingBottom = new BindableFloat { MinValue = 0, MaxValue = 200 }; + private readonly Bindable safeAreaPaddingLeft = new BindableFloat { MinValue = 0, MaxValue = 200 }; + private readonly Bindable safeAreaPaddingRight = new BindableFloat { MinValue = 0, MaxValue = 200 }; + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Usually this would be placed between the host and the game, but that's a bit of a pain to do with the test scene hierarchy. + + // Add is required for the container to get a size (and give out correct metrics to the usages in SafeAreaContainer). + Add( + safeAreaContainer = new SafeAreaDefiningContainer(safeArea = new BindableSafeArea()) + { + RelativeSizeAxes = Axes.Both + }); + + // Cache is required for the test game to see the safe area. + Dependencies.CacheAs(safeAreaContainer); + } + + public override void SetUpSteps() + { + AddStep("Add adjust controls", () => + { + Add(new Container + { + Depth = float.MinValue, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Alpha = 0.8f, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Width = 400, + Children = new Drawable[] + { + new SettingsSlider + { + Current = safeAreaPaddingTop, + LabelText = "Top" + }, + new SettingsSlider + { + Current = safeAreaPaddingBottom, + LabelText = "Bottom" + }, + new SettingsSlider + { + Current = safeAreaPaddingLeft, + LabelText = "Left" + }, + new SettingsSlider + { + Current = safeAreaPaddingRight, + LabelText = "Right" + }, + } + } + } + }); + + safeAreaPaddingTop.BindValueChanged(_ => updateSafeArea()); + safeAreaPaddingBottom.BindValueChanged(_ => updateSafeArea()); + safeAreaPaddingLeft.BindValueChanged(_ => updateSafeArea()); + safeAreaPaddingRight.BindValueChanged(_ => updateSafeArea()); + }); + + base.SetUpSteps(); + } + + private void updateSafeArea() + { + safeArea.Value = new MarginPadding + { + Top = safeAreaPaddingTop.Value, + Bottom = safeAreaPaddingBottom.Value, + Left = safeAreaPaddingLeft.Value, + Right = safeAreaPaddingRight.Value, + }; + } + + [Test] + public void TestSafeArea() + { + } + } +} From 30d2c7ba6a9355e88a3f605dd68d23728ede247a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 17:07:21 +0900 Subject: [PATCH 439/587] Add parenthesis to disambiguify conditionals --- osu.Game/Graphics/Containers/ScalingContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index f9bd571131..aa4e3a7fde 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -166,7 +166,7 @@ namespace osu.Game.Graphics.Containers var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One; var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero; - bool requiresMasking = scaling && targetSize != Vector2.One + bool requiresMasking = (scaling && targetSize != Vector2.One) // For the top level scaling container, for now we apply masking if safe areas are in use. // In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas. || (targetMode == ScalingMode.Everything && safeArea.SafeAreaPadding.Value.Total != Vector2.Zero); From b41655d5b905cd28425e2851f6900f24f7a82893 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Feb 2022 23:22:08 +0900 Subject: [PATCH 440/587] Fix crash when gameplay starts while in multi-spectator screen --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 1 + .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 27bf1c209c..8f6ba6375f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -914,6 +914,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for loading", () => client.Room?.State == MultiplayerRoomState.WaitingForLoad); AddStep("set player loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); AddUntilStep("wait for gameplay to start", () => client.Room?.State == MultiplayerRoomState.Playing); + AddUntilStep("wait for local user to enter spectator", () => multiplayerComponents.CurrentScreen is MultiSpectatorScreen); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 4bd68f2034..020217eac6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -457,6 +457,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } + // The beatmap is queried asynchronously when the selected item changes. + // This is an issue with MultiSpectatorScreen which is effectively in an always "ready" state and receives LoadRequested() callbacks + // even when it is not truly ready (i.e. the beatmap hasn't been selected by the client yet). For the time being, a simple fix to this is to ignore the callback. + // Note that spectator will be entered automatically when the client is capable of doing so via beatmap availability callbacks (see: updateBeatmapAvailability()). + if (client.LocalUser?.State == MultiplayerUserState.Spectating && Beatmap.IsDefault) + return; + StartPlay(); readyClickOperation?.Dispose(); From 0473c6c52f9156954eb7c782a421a540cbc35e5d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Feb 2022 17:53:30 +0900 Subject: [PATCH 441/587] Also handle null SelectedItem for safety --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 020217eac6..a397493bab 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -461,7 +461,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // This is an issue with MultiSpectatorScreen which is effectively in an always "ready" state and receives LoadRequested() callbacks // even when it is not truly ready (i.e. the beatmap hasn't been selected by the client yet). For the time being, a simple fix to this is to ignore the callback. // Note that spectator will be entered automatically when the client is capable of doing so via beatmap availability callbacks (see: updateBeatmapAvailability()). - if (client.LocalUser?.State == MultiplayerUserState.Spectating && Beatmap.IsDefault) + if (client.LocalUser?.State == MultiplayerUserState.Spectating && (SelectedItem.Value == null || Beatmap.IsDefault)) return; StartPlay(); From 8fc4d0c6f51053184b5d8952214aa652cf6f7c1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 18:20:17 +0900 Subject: [PATCH 442/587] Add override edge rule to overflow above home indicator on iOS --- osu.Game/OsuGameBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5f87abcfed..594e7a10c4 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -301,6 +301,7 @@ namespace osu.Game base.Content.Add(new SafeAreaContainer { + SafeAreaOverrideEdges = Edges.Bottom, RelativeSizeAxes = Axes.Both, Child = CreateScalingContainer().WithChildren(new Drawable[] { From 6457cf8d9b70264e46f757b01116b85ef393c163 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 18:45:38 +0900 Subject: [PATCH 443/587] Fix weird formatting in `TestSceneSafeArea` --- .../Visual/UserInterface/TestSceneSafeAreaHandling.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs index 676ae1276b..8b4e3f6d3a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs @@ -29,11 +29,10 @@ namespace osu.Game.Tests.Visual.UserInterface // Usually this would be placed between the host and the game, but that's a bit of a pain to do with the test scene hierarchy. // Add is required for the container to get a size (and give out correct metrics to the usages in SafeAreaContainer). - Add( - safeAreaContainer = new SafeAreaDefiningContainer(safeArea = new BindableSafeArea()) - { - RelativeSizeAxes = Axes.Both - }); + Add(safeAreaContainer = new SafeAreaDefiningContainer(safeArea = new BindableSafeArea()) + { + RelativeSizeAxes = Axes.Both + }); // Cache is required for the test game to see the safe area. Dependencies.CacheAs(safeAreaContainer); From 915d63f6dea742d853f7e635c3d18093d30de815 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 18:58:29 +0900 Subject: [PATCH 444/587] Limit safe area bottom override to iOS only --- osu.Game/OsuGameBase.cs | 8 +++++++- osu.iOS/OsuGameIOS.cs | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 594e7a10c4..97d2e64072 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -89,6 +89,12 @@ namespace osu.Game } } + /// + /// The that the game should be drawn over at a top level. + /// Defaults to . + /// + public virtual Edges SafeAreaOverrideEdges { get; set; } + protected OsuConfigManager LocalConfig { get; private set; } protected SessionStatics SessionStatics { get; private set; } @@ -301,7 +307,7 @@ namespace osu.Game base.Content.Add(new SafeAreaContainer { - SafeAreaOverrideEdges = Edges.Bottom, + SafeAreaOverrideEdges = SafeAreaOverrideEdges, RelativeSizeAxes = Axes.Both, Child = CreateScalingContainer().WithChildren(new Drawable[] { diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 702aef45f5..cf14745be1 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -3,6 +3,7 @@ using System; using Foundation; +using osu.Framework.Graphics; using osu.Game; using osu.Game.Updater; using osu.Game.Utils; @@ -18,6 +19,11 @@ namespace osu.iOS protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); + public override Edges SafeAreaOverrideEdges => + // iOS shows a home indicator at the bottom, and adds a safe area to account for this. + // Because we have the home indicator (mostly) hidden we don't really care about drawing in this region. + Edges.Bottom; + private class IOSBatteryInfo : BatteryInfo { public override double ChargeLevel => Battery.ChargeLevel; From dd63b1a3500be04b494623a51806121364720ba2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Feb 2022 19:03:52 +0900 Subject: [PATCH 445/587] Fix broken spectator playback test scene --- .../Gameplay/TestSceneSpectatorPlayback.cs | 221 +++++++----------- 1 file changed, 85 insertions(+), 136 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 4af254866a..69bf2a7a38 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -3,12 +3,8 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -20,7 +16,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; @@ -32,6 +27,7 @@ using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Tests.Visual.Spectator; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; @@ -47,138 +43,105 @@ namespace osu.Game.Tests.Visual.Gameplay private Replay replay; - private readonly IBindableList users = new BindableList(); - - private TestReplayRecorder recorder; - private ManualClock manualClock; private OsuSpriteText latencyDisplay; private TestFramedReplayInputHandler replayHandler; - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private SpectatorClient spectatorClient { get; set; } - - [Cached] - private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); - [SetUpSteps] public void SetUpSteps() { - AddStep("Reset recorder state", cleanUpState); - AddStep("Setup containers", () => { replay = new Replay(); manualClock = new ManualClock(); + SpectatorClient spectatorClient; + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new[] + { + (typeof(SpectatorClient), (object)(spectatorClient = new TestSpectatorClient())), + (typeof(GameplayState), new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty())) + }, + Children = new Drawable[] + { + spectatorClient, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + Recorder = new TestReplayRecorder + { + ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Brown, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Sending", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + }, + new Drawable[] + { + playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + Clock = new FramedClock(manualClock), + ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay) + { + GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.DarkBlue, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Receiving", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + } + } + }, + latencyDisplay = new OsuSpriteText() + } + }; spectatorClient.OnNewFrames += onNewFrames; - - users.BindTo(spectatorClient.PlayingUsers); - users.BindCollectionChanged((obj, args) => - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - Debug.Assert(args.NewItems != null); - - foreach (int user in args.NewItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.WatchUser(user); - } - - break; - - case NotifyCollectionChangedAction.Remove: - Debug.Assert(args.OldItems != null); - - foreach (int user in args.OldItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.StopWatchingUser(user); - } - - break; - } - }, true); - - Children = new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) - { - Recorder = recorder = new TestReplayRecorder - { - ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.Brown, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = "Sending", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestInputConsumer() - } - }, - } - }, - new Drawable[] - { - playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) - { - Clock = new FramedClock(manualClock), - ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay) - { - GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.DarkBlue, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = "Receiving", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestInputConsumer() - } - }, - } - } - } - }, - latencyDisplay = new OsuSpriteText() - }; }); } @@ -238,20 +201,6 @@ namespace osu.Game.Tests.Visual.Gameplay manualClock.CurrentTime = time.Value; } - [TearDownSteps] - public void TearDown() - { - AddStep("stop recorder", cleanUpState); - } - - private void cleanUpState() - { - // Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`. - recorder?.RemoveAndDisposeImmediately(); - recorder = null; - spectatorClient.OnNewFrames -= onNewFrames; - } - public class TestFramedReplayInputHandler : FramedReplayInputHandler { public TestFramedReplayInputHandler(Replay replay) From fa3d1115fa9535d288e9cc74756b3cc68548ed5b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Feb 2022 19:17:50 +0900 Subject: [PATCH 446/587] Remove online api requirement --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 69bf2a7a38..a4d8460846 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -36,8 +36,6 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneSpectatorPlayback : OsuManualInputManagerTestScene { - protected override bool UseOnlineAPI => true; - private TestRulesetInputManager playbackManager; private TestRulesetInputManager recordingManager; From 503025b970b1df6bb4935c13df7c4f9d82e56036 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 19:19:44 +0900 Subject: [PATCH 447/587] Fix completely incorrect and dangerous usage of bindable binding --- osu.Game/Graphics/Containers/ScalingContainer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index aa4e3a7fde..f505a62a33 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -23,6 +23,8 @@ namespace osu.Game.Graphics.Containers private Bindable posX; private Bindable posY; + private Bindable safeAreaPadding; + private readonly ScalingMode? targetMode; private Bindable scalingMode; @@ -101,11 +103,8 @@ namespace osu.Game.Graphics.Containers } } - [Resolved] - private ISafeArea safeArea { get; set; } - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, ISafeArea safeArea) { scalingMode = config.GetBindable(OsuSetting.Scaling); scalingMode.ValueChanged += _ => updateSize(); @@ -122,7 +121,8 @@ namespace osu.Game.Graphics.Containers posY = config.GetBindable(OsuSetting.ScalingPositionY); posY.ValueChanged += _ => updateSize(); - safeArea.SafeAreaPadding.BindValueChanged(_ => updateSize()); + safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy(); + safeAreaPadding.BindValueChanged(_ => updateSize()); } protected override void LoadComplete() @@ -169,7 +169,7 @@ namespace osu.Game.Graphics.Containers bool requiresMasking = (scaling && targetSize != Vector2.One) // For the top level scaling container, for now we apply masking if safe areas are in use. // In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas. - || (targetMode == ScalingMode.Everything && safeArea.SafeAreaPadding.Value.Total != Vector2.Zero); + || (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero); if (requiresMasking) sizableContainer.Masking = true; From 0f48c0131ca99a6b9f3283ebbd037cd991e181b6 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 4 Feb 2022 15:42:52 +0900 Subject: [PATCH 448/587] Layer playback of beatmap-changed and random-beatmap samples --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ++++ osu.Game/Screens/Select/SongSelect.cs | 21 +++------------------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f17daa8697..3d5ed70dda 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -154,6 +154,7 @@ namespace osu.Game.Screens.Select private readonly DrawablePool setPool = new DrawablePool(100); private Sample spinSample; + private Sample randomSelectSample; private int visibleSetsCount; @@ -178,6 +179,7 @@ namespace osu.Game.Screens.Select private void load(OsuConfigManager config, AudioManager audio) { spinSample = audio.Samples.Get("SongSelect/random-spin"); + randomSelectSample = audio.Samples.Get(@"SongSelect/select-random"); config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); @@ -495,6 +497,8 @@ namespace osu.Game.Screens.Select var chan = spinSample.GetChannel(); chan.Frequency.Value = 1f + Math.Min(1f, distance / visibleSetsCount); chan.Play(); + + randomSelectSample?.Play(); } private void select(CarouselItem item) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index ee807762bf..f5b11448f8 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -100,7 +100,6 @@ namespace osu.Game.Screens.Select private Sample sampleChangeDifficulty; private Sample sampleChangeBeatmap; - private Sample sampleRandomBeatmap; private Container carouselContainer; @@ -110,8 +109,6 @@ namespace osu.Game.Screens.Select private double audioFeedbackLastPlaybackTime; - private bool randomSelectionPending; - [Resolved] private MusicController music { get; set; } @@ -291,7 +288,6 @@ namespace osu.Game.Screens.Select sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); sampleChangeBeatmap = audio.Samples.Get(@"SongSelect/select-expand"); - sampleRandomBeatmap = audio.Samples.Get(@"SongSelect/select-random"); SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection"); if (dialogOverlay != null) @@ -319,16 +315,8 @@ namespace osu.Game.Screens.Select (new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom { - NextRandom = () => - { - randomSelectionPending = true; - Carousel.SelectNextRandom(); - }, - PreviousRandom = () => - { - randomSelectionPending = true; - Carousel.SelectPreviousRandom(); - } + NextRandom = () => Carousel.SelectNextRandom(), + PreviousRandom = Carousel.SelectPreviousRandom }, null), (new FooterButtonOptions(), BeatmapOptions) }; @@ -498,9 +486,7 @@ namespace osu.Game.Screens.Select { if (beatmap != null && beatmapInfoPrevious != null && Time.Current - audioFeedbackLastPlaybackTime >= 50) { - if (randomSelectionPending) - sampleRandomBeatmap.Play(); - else if (beatmap.BeatmapSet?.ID == beatmapInfoPrevious.BeatmapSet?.ID) + if (beatmap.BeatmapSet?.ID == beatmapInfoPrevious.BeatmapSet?.ID) sampleChangeDifficulty.Play(); else sampleChangeBeatmap.Play(); @@ -508,7 +494,6 @@ namespace osu.Game.Screens.Select audioFeedbackLastPlaybackTime = Time.Current; } - randomSelectionPending = false; beatmapInfoPrevious = beatmap; } From e2262bf3b25f14f625e76bec34118203299c31d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 20:33:15 +0900 Subject: [PATCH 449/587] Schedule all calls to `updateSize` for safety --- osu.Game/Graphics/Containers/ScalingContainer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index f505a62a33..b3423345b1 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Graphics.Containers return; allowScaling = value; - if (IsLoaded) updateSize(); + if (IsLoaded) Scheduler.AddOnce(updateSize); } } @@ -107,29 +107,29 @@ namespace osu.Game.Graphics.Containers private void load(OsuConfigManager config, ISafeArea safeArea) { scalingMode = config.GetBindable(OsuSetting.Scaling); - scalingMode.ValueChanged += _ => updateSize(); + scalingMode.ValueChanged += _ => Scheduler.AddOnce(updateSize); sizeX = config.GetBindable(OsuSetting.ScalingSizeX); - sizeX.ValueChanged += _ => updateSize(); + sizeX.ValueChanged += _ => Scheduler.AddOnce(updateSize); sizeY = config.GetBindable(OsuSetting.ScalingSizeY); - sizeY.ValueChanged += _ => updateSize(); + sizeY.ValueChanged += _ => Scheduler.AddOnce(updateSize); posX = config.GetBindable(OsuSetting.ScalingPositionX); - posX.ValueChanged += _ => updateSize(); + posX.ValueChanged += _ => Scheduler.AddOnce(updateSize); posY = config.GetBindable(OsuSetting.ScalingPositionY); - posY.ValueChanged += _ => updateSize(); + posY.ValueChanged += _ => Scheduler.AddOnce(updateSize); safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy(); - safeAreaPadding.BindValueChanged(_ => updateSize()); + safeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize)); } protected override void LoadComplete() { base.LoadComplete(); - updateSize(); + Scheduler.AddOnce(updateSize); sizableContainer.FinishTransforms(); } From b5dde6f1ad22d1c80502d307dee061556e3e404f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 21:38:52 +0900 Subject: [PATCH 450/587] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 04c543750e..71525a7acb 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 83c3593edb..c8f634284b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index b0c056ea21..5978f6d685 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + From 5e47ce333ce42831d8b4d48fdd4aa4437956587a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Feb 2022 16:10:49 +0300 Subject: [PATCH 451/587] Change `SafeAreaOverrideEdges` to be get-only and protected --- osu.Game/OsuGameBase.cs | 2 +- osu.iOS/OsuGameIOS.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 97d2e64072..5b2eb5607a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -93,7 +93,7 @@ namespace osu.Game /// The that the game should be drawn over at a top level. /// Defaults to . /// - public virtual Edges SafeAreaOverrideEdges { get; set; } + protected virtual Edges SafeAreaOverrideEdges => Edges.None; protected OsuConfigManager LocalConfig { get; private set; } diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index cf14745be1..9c1795e45e 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -19,7 +19,7 @@ namespace osu.iOS protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); - public override Edges SafeAreaOverrideEdges => + protected override Edges SafeAreaOverrideEdges => // iOS shows a home indicator at the bottom, and adds a safe area to account for this. // Because we have the home indicator (mostly) hidden we don't really care about drawing in this region. Edges.Bottom; From d62885f30b64cd2cfbac451591b952ff9663a5a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Feb 2022 22:31:41 +0900 Subject: [PATCH 452/587] Don't schedule call to `updateSize` in `LoadComplete` to ensure following `FinishTransforms` runs as expected Co-authored-by: Salman Ahmed --- osu.Game/Graphics/Containers/ScalingContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index b3423345b1..0d543bdbc8 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -129,7 +129,7 @@ namespace osu.Game.Graphics.Containers { base.LoadComplete(); - Scheduler.AddOnce(updateSize); + updateSize(); sizableContainer.FinishTransforms(); } From 750f90e7288b04f0abe4b7c4abb4805d52d5fda8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Feb 2022 22:38:40 +0900 Subject: [PATCH 453/587] Simplify TestSpectatorClient implementation --- .../Tests/Visual/Spectator/TestSpectatorClient.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 1a1d493249..453c086604 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -40,11 +39,7 @@ namespace osu.Game.Tests.Visual.Spectator public TestSpectatorClient() { - OnNewFrames += (i, bundle) => - { - if (PlayingUsers.Contains(i)) - lastReceivedUserFrames[i] = bundle.Frames[^1]; - }; + OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1]; } /// @@ -65,7 +60,7 @@ namespace osu.Game.Tests.Visual.Spectator /// The user to end play for. public void EndPlay(int userId) { - if (!PlayingUsers.Contains(userId)) + if (!userBeatmapDictionary.ContainsKey(userId)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState @@ -73,6 +68,8 @@ namespace osu.Game.Tests.Visual.Spectator BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, }); + + userBeatmapDictionary.Remove(userId); } public new void Schedule(Action action) => base.Schedule(action); @@ -131,7 +128,7 @@ namespace osu.Game.Tests.Visual.Spectator protected override Task WatchUserInternal(int userId) { // When newly watching a user, the server sends the playing state immediately. - if (PlayingUsers.Contains(userId)) + if (userBeatmapDictionary.ContainsKey(userId)) sendPlayingState(userId); return Task.CompletedTask; From eb25730b614ececc5f6672e298dc5e786984c92e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 5 Feb 2022 16:12:58 +0900 Subject: [PATCH 454/587] Revert "Merge pull request #16716 from peppy/carousel-less-invalidations" This reverts commit 8d13e0514b27ce5e6f587ba8918c165d3e1ddbbf, reversing changes made to 95582a9023488da37edb3b9236f6b4aeb197ccb9. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +- .../Screens/Select/Carousel/CarouselHeader.cs | 29 ++++---- .../Carousel/DrawableCarouselBeatmap.cs | 10 ++- .../Carousel/DrawableCarouselBeatmapSet.cs | 6 +- .../Select/Carousel/DrawableCarouselItem.cs | 71 +++++++------------ 5 files changed, 54 insertions(+), 68 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3d5ed70dda..c3d340ac61 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -925,8 +925,10 @@ namespace osu.Game.Screens.Select // child items (difficulties) are still visible. item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0); - // We are applying alpha to the header here such that we can layer alpha transformations on top. - item.Header.Alpha = Math.Clamp(1.75f - 1.5f * dist, 0, 1); + // We are applying a multiplicative alpha (which is internally done by nesting an + // additional container and setting that container's alpha) such that we can + // layer alpha transformations on top. + item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); } private enum PendingScrollOperation diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index 533694b265..ed3aea3445 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -21,6 +21,8 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselHeader : Container { + public Container BorderContainer; + public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); private readonly HoverLayer hoverLayer; @@ -35,14 +37,17 @@ namespace osu.Game.Screens.Select.Carousel RelativeSizeAxes = Axes.X; Height = DrawableCarouselItem.MAX_HEIGHT; - Masking = true; - CornerRadius = corner_radius; - BorderColour = new Color4(221, 255, 255, 255); - - InternalChildren = new Drawable[] + InternalChild = BorderContainer = new Container { - Content, - hoverLayer = new HoverLayer() + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius, + BorderColour = new Color4(221, 255, 255, 255), + Children = new Drawable[] + { + Content, + hoverLayer = new HoverLayer() + } }; } @@ -61,21 +66,21 @@ namespace osu.Game.Screens.Select.Carousel case CarouselItemState.NotSelected: hoverLayer.InsetForBorder = false; - BorderThickness = 0; - EdgeEffect = new EdgeEffectParameters + BorderContainer.BorderThickness = 0; + BorderContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Offset = new Vector2(1), Radius = 10, - Colour = Color4.Black.Opacity(0.5f), + Colour = Color4.Black.Opacity(100), }; break; case CarouselItemState.Selected: hoverLayer.InsetForBorder = true; - BorderThickness = border_thickness; - EdgeEffect = new EdgeEffectParameters + BorderContainer.BorderThickness = border_thickness; + BorderContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = new Color4(130, 204, 255, 150), diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index a3483aa60a..3576b77ae8 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -36,9 +36,9 @@ namespace osu.Game.Screens.Select.Carousel /// /// The height of a carousel beatmap, including vertical spacing. /// - public const float HEIGHT = header_height + CAROUSEL_BEATMAP_SPACING; + public const float HEIGHT = height + CAROUSEL_BEATMAP_SPACING; - private const float header_height = MAX_HEIGHT * 0.6f; + private const float height = MAX_HEIGHT * 0.6f; private readonly BeatmapInfo beatmapInfo; @@ -67,18 +67,16 @@ namespace osu.Game.Screens.Select.Carousel private CancellationTokenSource starDifficultyCancellationSource; public DrawableCarouselBeatmap(CarouselBeatmap panel) - : base(header_height) { beatmapInfo = panel.BeatmapInfo; Item = panel; - - // Difficulty panels should start hidden for a better initial effect. - Hide(); } [BackgroundDependencyLoader(true)] private void load(BeatmapManager manager, SongSelect songSelect) { + Header.Height = height; + if (songSelect != null) { startRequested = b => songSelect.FinaliseSelection(b); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 63c004f4bc..618c5cf5ec 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -122,10 +122,12 @@ namespace osu.Game.Screens.Select.Carousel }, }; - background.DelayedLoadComplete += d => d.FadeInFromZero(750, Easing.OutQuint); - mainFlow.DelayedLoadComplete += d => d.FadeInFromZero(500, Easing.OutQuint); + background.DelayedLoadComplete += fadeContentIn; + mainFlow.DelayedLoadComplete += fadeContentIn; } + private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint); + protected override void Deselected() { base.Deselected(); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 5e7ca0825a..cde3edad39 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -60,10 +60,12 @@ namespace osu.Game.Screens.Select.Carousel } } - protected DrawableCarouselItem(float headerHeight = MAX_HEIGHT) + protected DrawableCarouselItem() { RelativeSizeAxes = Axes.X; + Alpha = 0; + InternalChildren = new Drawable[] { MovementContainer = new Container @@ -71,20 +73,18 @@ namespace osu.Game.Screens.Select.Carousel RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - Header = new CarouselHeader - { - Height = headerHeight, - }, + Header = new CarouselHeader(), Content = new Container { RelativeSizeAxes = Axes.Both, - Y = headerHeight, } } }, }; } + public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha; + protected override void LoadComplete() { base.LoadComplete(); @@ -92,6 +92,12 @@ namespace osu.Game.Screens.Select.Carousel UpdateItem(); } + protected override void Update() + { + base.Update(); + Content.Y = Header.Height; + } + protected virtual void UpdateItem() { if (item == null) @@ -115,56 +121,29 @@ namespace osu.Game.Screens.Select.Carousel private void onStateChange(ValueChangedEvent _) => Scheduler.AddOnce(ApplyState); - private CarouselItemState? lastAppliedState; - protected virtual void ApplyState() { + // Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead. + // Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away. + Height = Item.TotalHeight; + Debug.Assert(Item != null); - if (lastAppliedState != Item.State.Value) + switch (Item.State.Value) { - lastAppliedState = Item.State.Value; + case CarouselItemState.NotSelected: + Deselected(); + break; - // Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead. - // Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away. - Height = Item.TotalHeight; - - switch (lastAppliedState) - { - case CarouselItemState.NotSelected: - Deselected(); - break; - - case CarouselItemState.Selected: - Selected(); - break; - } + case CarouselItemState.Selected: + Selected(); + break; } if (!Item.Visible) - Hide(); + this.FadeOut(300, Easing.OutQuint); else - Show(); - } - - private bool isVisible = true; - - public override void Show() - { - if (isVisible) - return; - - isVisible = true; - this.FadeIn(250); - } - - public override void Hide() - { - if (!isVisible) - return; - - isVisible = false; - this.FadeOut(300, Easing.OutQuint); + this.FadeIn(250); } protected virtual void Selected() From 2e1a9f137994a3085a592d216c075054126ceb38 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 21:13:16 +0800 Subject: [PATCH 455/587] Add performance breakdown as statistic item in extended statistics panel --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 11 + .../Statistics/PerformanceBreakdownChart.cs | 244 ++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 428e7b9df5..769e83ec00 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -298,6 +298,17 @@ namespace osu.Game.Rulesets.Osu } }, new StatisticRow + { + Columns = new[] + { + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }), + } + }, + new StatisticRow { Columns = new[] { diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs new file mode 100644 index 0000000000..b58974eb96 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -0,0 +1,244 @@ +// 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 System.Threading; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Scoring; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Ranking.Statistics +{ + public class PerformanceBreakdownChart : Container + { + private readonly ScoreInfo score; + + private Drawable spinner; + private Drawable content; + private GridContainer chart; + private OsuSpriteText achievedPerformance; + private OsuSpriteText maximumPerformance; + + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + + [Resolved] + private ScorePerformanceCache performanceCache { get; set; } + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + public PerformanceBreakdownChart(ScoreInfo score) + { + this.score = score; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new[] + { + spinner = new LoadingSpinner(true) + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre + }, + content = new FillFlowContainer + { + Alpha = 0, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.6f, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Spacing = new Vector2(15, 15), + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + Width = 0.8f, + AutoSizeAxes = Axes.Y, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Text = "Achieved PP", + Colour = Color4Extensions.FromHex("#66FFCC") + }, + achievedPerformance = new OsuSpriteText + { + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18), + Colour = Color4Extensions.FromHex("#66FFCC") + } + }, + new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Text = "Maximum", + Colour = OsuColour.Gray(0.7f) + }, + maximumPerformance = new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Colour = OsuColour.Gray(0.7f) + } + } + } + }, + chart = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + } + } + } + } + }; + + spinner.Show(); + + new PerformanceBreakdownCalculator(beatmapManager, difficultyCache, performanceCache) + .CalculateAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()))); + } + + private void setPerformanceValue(PerformanceBreakdown breakdown) + { + spinner.Hide(); + content.FadeIn(200); + + var displayAttributes = breakdown.Performance.GetAttributesForDisplay(); + var perfectDisplayAttributes = breakdown.PerfectPerformance.GetAttributesForDisplay(); + + setTotalValues( + displayAttributes.First(a => a.PropertyName == nameof(PerformanceAttributes.Total)), + perfectDisplayAttributes.First(a => a.PropertyName == nameof(PerformanceAttributes.Total)) + ); + + var rowDimensions = new List(); + var rows = new List(); + + foreach (PerformanceDisplayAttribute attr in displayAttributes) + { + if (attr.PropertyName == nameof(PerformanceAttributes.Total)) continue; + + var row = createAttributeRow(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)); + + if (row != null) + { + rows.Add(row); + rowDimensions.Add(new Dimension(GridSizeMode.AutoSize)); + } + } + + chart.RowDimensions = rowDimensions.ToArray(); + chart.Content = rows.ToArray(); + } + + private void setTotalValues(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) + { + achievedPerformance.Text = Math.Round(attribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(); + maximumPerformance.Text = Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(); + } + + [CanBeNull] + private Drawable[] createAttributeRow(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) + { + float percentage = (float)(attribute.Value / perfectAttribute.Value); + if (float.IsNaN(percentage)) + return null; + + return new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = attribute.DisplayName, + Colour = Colour4.White + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10, Right = 10 }, + Child = new Bar + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + CornerRadius = 2.5f, + Masking = true, + Height = 5, + BackgroundColour = Color4.White.Opacity(0.5f), + AccentColour = Color4Extensions.FromHex("#66FFCC"), + Length = percentage + } + }, + new OsuSpriteText + { + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Text = percentage.ToLocalisableString("0%"), + Colour = Colour4.White + } + }; + } + + protected override void Dispose(bool isDisposing) + { + cancellationTokenSource?.Cancel(); + base.Dispose(isDisposing); + } + } +} From c35ef917a171564750d54c3bee09d24339d27ee2 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 21:18:23 +0800 Subject: [PATCH 456/587] Remove tooltip from performance statistic --- .../Statistics/PerformanceStatistic.cs | 43 ++-- .../Statistics/PerformanceStatisticTooltip.cs | 212 ------------------ 2 files changed, 14 insertions(+), 241 deletions(-) delete mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 158fd82b29..859b42d66d 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -2,21 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Cursor; -using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets.Difficulty; using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatistic : StatisticDisplay, IHasCustomTooltip + public class PerformanceStatistic : StatisticDisplay { private readonly ScoreInfo score; @@ -26,15 +22,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private RollingCounter counter; - [Resolved] - private ScorePerformanceCache performanceCache { get; set; } - - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } - - [Resolved] - private BeatmapManager beatmapManager { get; set; } - public PerformanceStatistic(ScoreInfo score) : base("PP") { @@ -42,21 +29,23 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics } [BackgroundDependencyLoader] - private void load() + private void load(ScorePerformanceCache performanceCache) { - new PerformanceBreakdownCalculator(beatmapManager, difficultyCache, performanceCache) - .CalculateAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()))); + if (score.PP.HasValue) + { + setPerformanceValue(score.PP.Value); + } + else + { + performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely().Total)), cancellationTokenSource.Token); + } } - private void setPerformanceValue(PerformanceBreakdown breakdown) + private void setPerformanceValue(double? pp) { - // Don't display the tooltip if "Total" is the only item - if (breakdown != null && breakdown.Performance.GetAttributesForDisplay().Count() > 1) - { - TooltipContent = breakdown; - performance.Value = (int)Math.Round(breakdown.Performance.Total, MidpointRounding.AwayFromZero); - } + if (pp.HasValue) + performance.Value = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); } public override void Appear() @@ -76,9 +65,5 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }; - - public ITooltip GetCustomTooltip() => new PerformanceStatisticTooltip(); - - public PerformanceBreakdown TooltipContent { get; private set; } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs deleted file mode 100644 index bd6eb057c6..0000000000 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatisticTooltip.cs +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets.Difficulty; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Ranking.Expanded.Statistics -{ - public class PerformanceStatisticTooltip : VisibilityContainer, ITooltip - { - private readonly Box background; - private Colour4 titleColor; - private Colour4 textColour; - - protected override Container Content { get; } - - public PerformanceStatisticTooltip() - { - AutoSizeAxes = Axes.Both; - Masking = true; - CornerRadius = 5; - - InternalChildren = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both - }, - Content = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 } - }, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - background.Colour = colours.Gray3; - titleColor = colours.Blue; - textColour = colours.BlueLighter; - } - - protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); - - protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); - - private PerformanceBreakdown currentPerformance; - - public void SetContent(PerformanceBreakdown performance) - { - if (performance == currentPerformance) - return; - - currentPerformance = performance; - - UpdateDisplay(performance); - } - - private Drawable createItemForTotal(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) - { - return - new GridContainer - { - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Bottom = 10 }, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 250), - new Dimension(GridSizeMode.AutoSize) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = attribute.DisplayName, - Colour = titleColor - }, - new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = Math.Round(attribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(), - Colour = titleColor - } - }, - new Drawable[] - { - new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = "Maximum", - Colour = OsuColour.Gray(0.7f) - }, - new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(), - Colour = OsuColour.Gray(0.7f) - } - } - } - }; - } - - private Drawable createItemForAttribute(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) - { - float percentage = (float)(attribute.Value / perfectAttribute.Value); - if (float.IsNaN(percentage)) - return null; - - return new GridContainer - { - AutoSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 110), - new Dimension(GridSizeMode.Absolute, 140), - new Dimension(GridSizeMode.AutoSize) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = attribute.DisplayName, - Colour = textColour - }, - new Bar - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Width = 130, - Height = 5, - BackgroundColour = Color4.White.Opacity(0.5f), - Colour = textColour, - Length = percentage, - Margin = new MarginPadding { Left = 5, Right = 5 } - }, - new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = percentage.ToLocalisableString("0%"), - Colour = textColour - } - } - } - }; - } - - protected virtual void UpdateDisplay(PerformanceBreakdown performance) - { - Content.Clear(); - - var displayAttributes = performance.Performance.GetAttributesForDisplay(); - - var perfectDisplayAttributes = performance.PerfectPerformance.GetAttributesForDisplay(); - - foreach (PerformanceDisplayAttribute attr in displayAttributes) - { - var attributeItem = attr.PropertyName == nameof(PerformanceAttributes.Total) - ? createItemForTotal(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)) - : createItemForAttribute(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)); - - if (attributeItem != null) - Content.Add(attributeItem); - } - } - - public void Move(Vector2 pos) => Position = pos; - } -} From 440b674bb017f54affd7f201580936ced8a8a126 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 21:30:35 +0800 Subject: [PATCH 457/587] Add statistic item for mania & taiko --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 11 +++++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index ffb26b224f..aa9b4d5f1d 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -378,6 +378,17 @@ namespace osu.Game.Rulesets.Mania } }, new StatisticRow + { + Columns = new[] + { + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }), + } + }, + new StatisticRow { Columns = new[] { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 21c99c0d2f..37e959f8b4 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -221,6 +221,17 @@ namespace osu.Game.Rulesets.Taiko } }, new StatisticRow + { + Columns = new[] + { + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }), + } + }, + new StatisticRow { Columns = new[] { From 0b1fef38af1660d6c17c87bdb0c94cff9770cfb0 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 21:36:34 +0800 Subject: [PATCH 458/587] Use the playable beatmap provided in `CreateStatisticsForScore` --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- .../PerformanceBreakdownCalculator.cs | 17 ++++++++--------- .../Statistics/PerformanceBreakdownChart.cs | 9 ++++----- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index aa9b4d5f1d..9027b71069 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -381,7 +381,7 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 769e83ec00..68e91e42a7 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -301,7 +301,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 37e959f8b4..ddc594a70a 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 46342b237c..d68c2edb40 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Difficulty { public class PerformanceBreakdownCalculator { - private readonly BeatmapManager beatmapManager; + private readonly IBeatmap playableBeatmap; private readonly BeatmapDifficultyCache difficultyCache; private readonly ScorePerformanceCache performanceCache; - public PerformanceBreakdownCalculator(BeatmapManager beatmapManager, BeatmapDifficultyCache difficultyCache, ScorePerformanceCache performanceCache) + public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache, ScorePerformanceCache performanceCache) { - this.beatmapManager = beatmapManager; + this.playableBeatmap = playableBeatmap; this.difficultyCache = difficultyCache; this.performanceCache = performanceCache; } @@ -46,14 +46,13 @@ namespace osu.Game.Rulesets.Difficulty return Task.Run(async () => { Ruleset ruleset = score.Ruleset.CreateInstance(); - IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(score.BeatmapInfo).GetPlayableBeatmap(score.Ruleset, score.Mods); ScoreInfo perfectPlay = score.DeepClone(); perfectPlay.Accuracy = 1; perfectPlay.Passed = true; // calculate max combo var difficulty = await difficultyCache.GetDifficultyAsync( - beatmap.BeatmapInfo, + playableBeatmap.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken @@ -65,10 +64,10 @@ namespace osu.Game.Rulesets.Difficulty perfectPlay.MaxCombo = difficulty.Value.MaxCombo; // create statistics assuming all hit objects have perfect hit result - var statistics = beatmap.HitObjects - .SelectMany(getPerfectHitResults) - .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) - .ToDictionary(pair => pair.hitResult, pair => pair.count); + var statistics = playableBeatmap.HitObjects + .SelectMany(getPerfectHitResults) + .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) + .ToDictionary(pair => pair.hitResult, pair => pair.count); perfectPlay.Statistics = statistics; // calculate total score diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index b58974eb96..fb9c93f0e6 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -26,6 +26,7 @@ namespace osu.Game.Screens.Ranking.Statistics public class PerformanceBreakdownChart : Container { private readonly ScoreInfo score; + private readonly IBeatmap playableBeatmap; private Drawable spinner; private Drawable content; @@ -41,12 +42,10 @@ namespace osu.Game.Screens.Ranking.Statistics [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } - [Resolved] - private BeatmapManager beatmapManager { get; set; } - - public PerformanceBreakdownChart(ScoreInfo score) + public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap) { this.score = score; + this.playableBeatmap = playableBeatmap; } [BackgroundDependencyLoader] @@ -146,7 +145,7 @@ namespace osu.Game.Screens.Ranking.Statistics spinner.Show(); - new PerformanceBreakdownCalculator(beatmapManager, difficultyCache, performanceCache) + new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache, performanceCache) .CalculateAsync(score, cancellationTokenSource.Token) .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()))); } From ee6d4b25836d8051e4802b02c68075c28e2494b4 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 21:39:01 +0800 Subject: [PATCH 459/587] Move performance breakdown to the top to prevent re-ordering after watching replay --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 12 ++++++------ osu.Game.Rulesets.Osu/OsuRuleset.cs | 22 +++++++++++----------- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 12 ++++++------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 9027b71069..180b9ef71b 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -370,22 +370,22 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, - Height = 250 - }, true), + AutoSizeAxes = Axes.Y + }), } }, new StatisticRow { Columns = new[] { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents) { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }), + Height = 250 + }, true), } }, new StatisticRow diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 68e91e42a7..ad00a025a1 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -275,6 +275,17 @@ namespace osu.Game.Rulesets.Osu return new[] { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }), + } + }, new StatisticRow { Columns = new[] @@ -298,17 +309,6 @@ namespace osu.Game.Rulesets.Osu } }, new StatisticRow - { - Columns = new[] - { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }), - } - }, - new StatisticRow { Columns = new[] { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index ddc594a70a..e56aabaf9d 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -213,22 +213,22 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, - Height = 250 - }, true), + AutoSizeAxes = Axes.Y + }), } }, new StatisticRow { Columns = new[] { - new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }), + Height = 250 + }, true), } }, new StatisticRow From b31c1513f6ef0e7c17e7a3af39bb19174084255a Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Feb 2022 22:41:04 +0800 Subject: [PATCH 460/587] Fix test failure The cursor was clicking too far to the right. --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 988f429ff5..167acc94c4 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("click to right of panel", () => { var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); - InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(100, 0)); + InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(50, 0)); InputManager.Click(MouseButton.Left); }); From f78c853bc752f33d5fff6688fd286182e8ecd476 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 6 Feb 2022 10:59:53 +0800 Subject: [PATCH 461/587] Calculate max combo locally in `PerformanceBreakdownCalculator` --- .../PerformanceBreakdownCalculator.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index d68c2edb40..87e60fbe9d 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -51,17 +51,7 @@ namespace osu.Game.Rulesets.Difficulty perfectPlay.Passed = true; // calculate max combo - var difficulty = await difficultyCache.GetDifficultyAsync( - playableBeatmap.BeatmapInfo, - score.Ruleset, - score.Mods, - cancellationToken - ).ConfigureAwait(false); - - if (difficulty == null) - return null; - - perfectPlay.MaxCombo = difficulty.Value.MaxCombo; + perfectPlay.MaxCombo = calculateMaxCombo(playableBeatmap); // create statistics assuming all hit objects have perfect hit result var statistics = playableBeatmap.HitObjects @@ -86,11 +76,23 @@ namespace osu.Game.Rulesets.Difficulty } // calculate performance for this perfect score + var difficulty = await difficultyCache.GetDifficultyAsync( + playableBeatmap.BeatmapInfo, + score.Ruleset, + score.Mods, + cancellationToken + ).ConfigureAwait(false); + // ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes - return ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate(); + return difficulty == null ? null : ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate(); }, cancellationToken); } + private int calculateMaxCombo(IBeatmap beatmap) + { + return beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo()); + } + private IEnumerable getPerfectHitResults(HitObject hitObject) { foreach (HitObject nested in hitObject.NestedHitObjects) From 56c90a21ceec78749b3b76f26c46c54cb3533169 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 6 Feb 2022 11:22:12 +0800 Subject: [PATCH 462/587] Add a todo --- osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 87e60fbe9d..3d384f5914 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -51,6 +51,7 @@ namespace osu.Game.Rulesets.Difficulty perfectPlay.Passed = true; // calculate max combo + // todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores perfectPlay.MaxCombo = calculateMaxCombo(playableBeatmap); // create statistics assuming all hit objects have perfect hit result From c2416c490e3b81c1fe7aa61c90200f9757fed7c8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 11:29:39 +0900 Subject: [PATCH 463/587] Fix crash on disconnection during multi-spectate --- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 4646f42d63..bff7023102 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -228,7 +227,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public override bool OnBackButton() { - Debug.Assert(multiplayerClient.Room != null); + if (multiplayerClient.Room == null) + return base.OnBackButton(); // On a manual exit, set the player back to idle unless gameplay has finished. if (multiplayerClient.Room.State != MultiplayerRoomState.Open) From 10bdb7240ff3615f53cb7a0ce61b88f5b3d3428c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 8 Feb 2022 14:36:29 +0800 Subject: [PATCH 464/587] Pre-check for divisor zero and add explanation --- .../Ranking/Statistics/PerformanceBreakdownChart.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index fb9c93f0e6..5b42554716 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -12,6 +12,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -192,10 +193,13 @@ namespace osu.Game.Screens.Ranking.Statistics [CanBeNull] private Drawable[] createAttributeRow(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) { - float percentage = (float)(attribute.Value / perfectAttribute.Value); - if (float.IsNaN(percentage)) + // Don't display the attribute if its maximum is 0 + // For example, flashlight bonus would be zero if flashlight mod isn't on + if (Precision.AlmostEquals(perfectAttribute.Value, 0f)) return null; + float percentage = (float)(attribute.Value / perfectAttribute.Value); + return new Drawable[] { new OsuSpriteText From 9c2d57d70783cdc71c8a8ee5439dcbc5eec41073 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 19:36:16 +0900 Subject: [PATCH 465/587] Add failing test --- .../Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 94e61eaee0..343fc7e6e0 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -92,7 +92,8 @@ namespace osu.Game.Tests.Online AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); AddUntilStep("wait for import", () => beatmaps.CurrentImport != null); - addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); + AddAssert("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet)); + addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable); } [Test] From b1a73996ba69de0cd1e02c9cb25266d52d29e2b7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 19:36:41 +0900 Subject: [PATCH 466/587] Fix incorrect check for beatmap availability --- osu.Game/Stores/BeatmapImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 709dd67087..2a2786b861 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -165,7 +165,7 @@ namespace osu.Game.Stores public override bool IsAvailableLocally(BeatmapSetInfo model) { - return Realm.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); + return Realm.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); } public override string HumanisedModelName => "beatmap"; From 0d99017178af1b373212f4174242e7bf8ee224a3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 20:27:08 +0900 Subject: [PATCH 467/587] Add state tests --- .../Visual/Gameplay/TestSceneSpectator.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 9f8470446c..65583919d6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -235,6 +235,71 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("last frame has header", () => lastBundle.Frames[^1].Header != null); } + [Test] + public void TestPlayingState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + + [Test] + public void TestCompletionState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send completion", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Completed)); + AddUntilStep("state is completed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Completed); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + + [Test] + public void TestQuitState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Quit)); + AddUntilStep("state is quit", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Quit); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + + [Test] + public void TestFailedState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Failed)); + AddUntilStep("state is failed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Failed); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; From 4c76027178933d9bcade478495f0588746220a18 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 20:29:49 +0900 Subject: [PATCH 468/587] Rename completed state to passed --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 6 +++--- osu.Game/Online/Spectator/SpectatingUserState.cs | 10 +++++----- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 65583919d6..a263bb1e6f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -247,7 +247,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestCompletionState() + public void TestPassedState() { loadSpectatingScreen(); @@ -255,8 +255,8 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send completion", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Completed)); - AddUntilStep("state is completed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Completed); + AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Passed)); + AddUntilStep("state is passed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Passed); start(); sendFrames(); diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatingUserState.cs index c7ba4ba248..30be57b739 100644 --- a/osu.Game/Online/Spectator/SpectatingUserState.cs +++ b/osu.Game/Online/Spectator/SpectatingUserState.cs @@ -6,7 +6,7 @@ namespace osu.Game.Online.Spectator public enum SpectatingUserState { /// - /// The spectated user has not yet played. + /// The spectated user is not yet playing. /// Idle, @@ -16,17 +16,17 @@ namespace osu.Game.Online.Spectator Playing, /// - /// The spectated user has successfully completed gameplay. + /// The spectated user has passed gameplay. /// - Completed, + Passed, /// - /// The spectator user has failed during gameplay. + /// The spectator user has failed gameplay. /// Failed, /// - /// The spectated user has quit during gameplay. + /// The spectated user has quit gameplay. /// Quit } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 9e168411b0..ee5fdc917a 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -186,7 +186,7 @@ namespace osu.Game.Online.Spectator currentBeatmap = null; if (state.HasPassed) - currentState.State = SpectatingUserState.Completed; + currentState.State = SpectatingUserState.Passed; else if (state.HasFailed) currentState.State = SpectatingUserState.Failed; else diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 125e4b261c..60bf87ff61 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -216,7 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void EndGameplay(int userId, SpectatorState state) { - if (state.State == SpectatingUserState.Completed || state.State == SpectatingUserState.Failed) + if (state.State == SpectatingUserState.Passed || state.State == SpectatingUserState.Failed) return; RemoveUser(userId); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 7b58f669a0..394afb3a4c 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -134,7 +134,7 @@ namespace osu.Game.Screens.Spectate switch (newState.State) { - case SpectatingUserState.Completed: + case SpectatingUserState.Passed: // Make sure that gameplay completes to the end. if (gameplayStates.TryGetValue(userId, out var gameplayState)) gameplayState.Score.Replay.HasReceivedAllFrames = true; From c1766d8a411f1626255c1d74fbf0b634e7bc4610 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 20:29:53 +0900 Subject: [PATCH 469/587] Add paused state --- osu.Game/Online/Spectator/SpectatingUserState.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatingUserState.cs index 30be57b739..e00c2e1947 100644 --- a/osu.Game/Online/Spectator/SpectatingUserState.cs +++ b/osu.Game/Online/Spectator/SpectatingUserState.cs @@ -15,6 +15,11 @@ namespace osu.Game.Online.Spectator /// Playing, + /// + /// The spectated user is currently paused. Unused for the time being. + /// + Paused, + /// /// The spectated user has passed gameplay. /// From 79d1d54e336c2cb0eff7399912933eb32f24b19e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Feb 2022 20:35:38 +0900 Subject: [PATCH 470/587] Rename parameter to match other usages --- osu.Game/Stores/BeatmapImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 2a2786b861..e6b655589c 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -165,7 +165,7 @@ namespace osu.Game.Stores public override bool IsAvailableLocally(BeatmapSetInfo model) { - return Realm.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); + return Realm.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); } public override string HumanisedModelName => "beatmap"; From 886d1d2df637dcbac1e3b7147b3062ba576ce178 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 21:20:33 +0900 Subject: [PATCH 471/587] Refactorings --- .../Visual/Gameplay/TestSceneSpectator.cs | 3 ++- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- .../Dashboard/CurrentlyPlayingDisplay.cs | 20 +++++++++---------- .../Spectate/MultiSpectatorScreen.cs | 2 ++ osu.Game/Screens/Play/GameplayState.cs | 2 +- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index a263bb1e6f..e0946df2dc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -160,7 +160,8 @@ namespace osu.Game.Tests.Visual.Gameplay finish(SpectatingUserState.Failed); checkPaused(false); // Should continue playing until out of frames - checkPaused(true); + checkPaused(true); // And eventually stop after running out of frames and fail. + // Todo: Should check for + display a failed message. } [Test] diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index ee5fdc917a..ed6e355ddf 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -98,8 +98,8 @@ namespace osu.Game.Online.Spectator } else { - watchingUserStates.Clear(); playingUsers.Clear(); + watchingUserStates.Clear(); } }), true); } diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 02ef28f825..117de88166 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -69,17 +69,17 @@ namespace osu.Game.Overlays.Dashboard { var user = task.GetResultSafely(); - if (user != null) - { - Schedule(() => - { - // user may no longer be playing. - if (!playingUsers.Contains(user.Id)) - return; + if (user == null) + return; - userFlow.Add(createUserPanel(user)); - }); - } + Schedule(() => + { + // user may no longer be playing. + if (!playingUsers.Contains(user.Id)) + return; + + userFlow.Add(createUserPanel(user)); + }); }); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 60bf87ff61..55df8301da 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -216,6 +216,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void EndGameplay(int userId, SpectatorState state) { + // Allowed passed/failed users to complete their remaining replay frames. + // The failed state isn't really possible in multiplayer (yet?) but is added here just for safety in case it starts being used. if (state.State == SpectatingUserState.Passed || state.State == SpectatingUserState.Failed) return; diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 64e873b3bb..c6a072da74 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play public bool HasFailed { get; set; } /// - /// Whether the user quit gameplay without either having either passed or failed. + /// Whether the user quit gameplay without having either passed or failed. /// public bool HasQuit { get; set; } From c242a63b1177b4e772fc52dff5b4eb2030bd48b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Feb 2022 10:16:43 +0900 Subject: [PATCH 472/587] Fix playlist overlay null reference when attempting an empty selection As reported at https://github.com/ppy/osu/discussions/16829. --- osu.Game/Overlays/Music/PlaylistOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 59ade0918d..ce816f84f0 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Music filter.Search.OnCommit += (sender, newText) => { - list.FirstVisibleSet.PerformRead(set => + list.FirstVisibleSet?.PerformRead(set => { BeatmapInfo toSelect = set.Beatmaps.FirstOrDefault(); From 4966c4e974c9a3e7b98910adeee5b658d6889e22 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Feb 2022 11:51:47 +0900 Subject: [PATCH 473/587] Remove redundant parameter --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index e0946df2dc..7235bce789 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -274,7 +274,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Quit)); + AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id)); AddUntilStep("state is quit", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Quit); start(); From ffc4c64f7e7fb0951c9c3f3109497b37f50196b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Feb 2022 12:09:04 +0900 Subject: [PATCH 474/587] Unify namings across the board --- .../Visual/Gameplay/TestSceneSpectator.cs | 22 ++++++------ .../Visual/Gameplay/TestSceneSpectatorHost.cs | 4 +-- ...TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- ...tingUserState.cs => SpectatedUserState.cs} | 4 +-- osu.Game/Online/Spectator/SpectatorClient.cs | 36 +++++++++---------- osu.Game/Online/Spectator/SpectatorState.cs | 2 +- .../Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 6 ++-- .../Visual/Spectator/TestSpectatorClient.cs | 4 +-- 9 files changed, 41 insertions(+), 41 deletions(-) rename osu.Game/Online/Spectator/{SpectatingUserState.cs => SpectatedUserState.cs} (90%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 7235bce789..157c248d69 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Gameplay checkPaused(true); sendFrames(); - finish(SpectatingUserState.Failed); + finish(SpectatedUserState.Failed); checkPaused(false); // Should continue playing until out of frames checkPaused(true); // And eventually stop after running out of frames and fail. @@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] @@ -256,13 +256,13 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Passed)); - AddUntilStep("state is passed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Passed); + AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Passed)); + AddUntilStep("state is passed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Passed); start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] @@ -275,12 +275,12 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id)); - AddUntilStep("state is quit", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Quit); + AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit); start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] @@ -292,13 +292,13 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Failed)); - AddUntilStep("state is failed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Failed); + AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Failed)); + AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } private OsuFramedReplayInputHandler replayHandler => @@ -313,7 +313,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); - private void finish(SpectatingUserState state = SpectatingUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state)); + private void finish(SpectatedUserState state = SpectatedUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state)); private void checkPaused(bool state) => AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs index 6d6b0bf89e..034519fbf8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs @@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClientSendsCorrectRuleset() { - AddUntilStep("spectator client sending frames", () => spectatorClient.WatchingUserStates.ContainsKey(dummy_user_id)); - AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); + AddUntilStep("spectator client sending frames", () => spectatorClient.WatchedUserStates.ContainsKey(dummy_user_id)); + AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchedUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); } public override void TearDownSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 55450b36e2..1322fbc96e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void RandomlyUpdateState() { - foreach ((int userId, _) in WatchingUserStates) + foreach ((int userId, _) in WatchedUserStates) { if (RNG.NextBool()) continue; diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatedUserState.cs similarity index 90% rename from osu.Game/Online/Spectator/SpectatingUserState.cs rename to osu.Game/Online/Spectator/SpectatedUserState.cs index e00c2e1947..0f0a3068b8 100644 --- a/osu.Game/Online/Spectator/SpectatingUserState.cs +++ b/osu.Game/Online/Spectator/SpectatedUserState.cs @@ -3,7 +3,7 @@ namespace osu.Game.Online.Spectator { - public enum SpectatingUserState + public enum SpectatedUserState { /// /// The spectated user is not yet playing. @@ -26,7 +26,7 @@ namespace osu.Game.Online.Spectator Passed, /// - /// The spectator user has failed gameplay. + /// The spectated user has failed gameplay. /// Failed, diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index ed6e355ddf..a54ea0d9ee 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -38,7 +38,7 @@ namespace osu.Game.Online.Spectator /// /// The states of all users currently being watched. /// - public IBindableDictionary WatchingUserStates => watchingUserStates; + public IBindableDictionary WatchedUserStates => watchedUserStates; /// /// A global list of all players currently playing. @@ -48,9 +48,9 @@ namespace osu.Game.Online.Spectator /// /// All users currently being watched. /// - private readonly List watchingUsers = new List(); + private readonly List watchedUsers = new List(); - private readonly BindableDictionary watchingUserStates = new BindableDictionary(); + private readonly BindableDictionary watchedUserStates = new BindableDictionary(); private readonly BindableList playingUsers = new BindableList(); private readonly SpectatorState currentState = new SpectatorState(); @@ -85,8 +85,8 @@ namespace osu.Game.Online.Spectator if (connected.NewValue) { // get all the users that were previously being watched - int[] users = watchingUsers.ToArray(); - watchingUsers.Clear(); + int[] users = watchedUsers.ToArray(); + watchedUsers.Clear(); // resubscribe to watched users. foreach (int userId in users) @@ -99,7 +99,7 @@ namespace osu.Game.Online.Spectator else { playingUsers.Clear(); - watchingUserStates.Clear(); + watchedUserStates.Clear(); } }), true); } @@ -111,8 +111,8 @@ namespace osu.Game.Online.Spectator if (!playingUsers.Contains(userId)) playingUsers.Add(userId); - if (watchingUsers.Contains(userId)) - watchingUserStates[userId] = state; + if (watchedUsers.Contains(userId)) + watchedUserStates[userId] = state; OnUserBeganPlaying?.Invoke(userId, state); }); @@ -126,8 +126,8 @@ namespace osu.Game.Online.Spectator { playingUsers.Remove(userId); - if (watchingUsers.Contains(userId)) - watchingUserStates[userId] = state; + if (watchedUsers.Contains(userId)) + watchedUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); }); @@ -159,7 +159,7 @@ namespace osu.Game.Online.Spectator currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); - currentState.State = SpectatingUserState.Playing; + currentState.State = SpectatedUserState.Playing; currentBeatmap = state.Beatmap; currentScore = score; @@ -186,11 +186,11 @@ namespace osu.Game.Online.Spectator currentBeatmap = null; if (state.HasPassed) - currentState.State = SpectatingUserState.Passed; + currentState.State = SpectatedUserState.Passed; else if (state.HasFailed) - currentState.State = SpectatingUserState.Failed; + currentState.State = SpectatedUserState.Failed; else - currentState.State = SpectatingUserState.Quit; + currentState.State = SpectatedUserState.Quit; EndPlayingInternal(currentState); }); @@ -200,10 +200,10 @@ namespace osu.Game.Online.Spectator { Debug.Assert(ThreadSafety.IsUpdateThread); - if (watchingUsers.Contains(userId)) + if (watchedUsers.Contains(userId)) return; - watchingUsers.Add(userId); + watchedUsers.Add(userId); WatchUserInternal(userId); } @@ -214,8 +214,8 @@ namespace osu.Game.Online.Spectator // Todo: This should not be a thing, but requires framework changes. Schedule(() => { - watchingUsers.Remove(userId); - watchingUserStates.Remove(userId); + watchedUsers.Remove(userId); + watchedUserStates.Remove(userId); StopWatchingUserInternal(userId); }); } diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index fc62f16bba..77686d12da 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -25,7 +25,7 @@ namespace osu.Game.Online.Spectator public IEnumerable Mods { get; set; } = Enumerable.Empty(); [Key(3)] - public SpectatingUserState State { get; set; } + public SpectatedUserState State { get; set; } public bool Equals(SpectatorState other) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 55df8301da..571b6b4324 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -218,7 +218,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { // Allowed passed/failed users to complete their remaining replay frames. // The failed state isn't really possible in multiplayer (yet?) but is added here just for safety in case it starts being used. - if (state.State == SpectatingUserState.Passed || state.State == SpectatingUserState.Failed) + if (state.State == SpectatedUserState.Passed || state.State == SpectatedUserState.Failed) return; RemoveUser(userId); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 394afb3a4c..9d0f8abddc 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Spectate userMap[u.Id] = u; } - userStates.BindTo(spectatorClient.WatchingUserStates); + userStates.BindTo(spectatorClient.WatchedUserStates); userStates.BindCollectionChanged(onUserStatesChanged, true); realmSubscription = realm.RegisterForNotifications( @@ -134,13 +134,13 @@ namespace osu.Game.Screens.Spectate switch (newState.State) { - case SpectatingUserState.Passed: + case SpectatedUserState.Passed: // Make sure that gameplay completes to the end. if (gameplayStates.TryGetValue(userId, out var gameplayState)) gameplayState.Score.Replay.HasReceivedAllFrames = true; break; - case SpectatingUserState.Playing: + case SpectatedUserState.Playing: Schedule(() => OnUserStateChanged(userId, newState)); updateGameplayState(userId); break; diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 6862cda88c..1322a99ea7 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Spectator /// /// The user to end play for. /// The spectator state to end play with. - public void EndPlay(int userId, SpectatingUserState state = SpectatingUserState.Quit) + public void EndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit) { if (!userBeatmapDictionary.ContainsKey(userId)) return; @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.Spectator { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, - State = SpectatingUserState.Playing + State = SpectatedUserState.Playing }); } } From 18251c9285a559ac3f4070cdc8538211175e18ab Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Feb 2022 12:20:07 +0900 Subject: [PATCH 475/587] Clean up SpectatorScreen based on suggestions --- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/Play/SoloSpectator.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 571b6b4324..e5eeeb3448 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -207,7 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } } - protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) + protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) { } diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index a710db6d24..a0b07fcbd9 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -166,7 +166,7 @@ namespace osu.Game.Screens.Play automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) + protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) { clearDisplay(); showBeatmapPanel(spectatorState); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 9d0f8abddc..9eb374f0f7 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Spectate continue; if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID)) - updateGameplayState(userId); + startGameplay(userId); } } @@ -141,8 +141,8 @@ namespace osu.Game.Screens.Spectate break; case SpectatedUserState.Playing: - Schedule(() => OnUserStateChanged(userId, newState)); - updateGameplayState(userId); + Schedule(() => OnNewPlayingUserState(userId, newState)); + startGameplay(userId); break; } } @@ -161,7 +161,7 @@ namespace osu.Game.Screens.Spectate Schedule(() => EndGameplay(userId, state)); } - private void updateGameplayState(int userId) + private void startGameplay(int userId) { Debug.Assert(userMap.ContainsKey(userId)); @@ -195,11 +195,11 @@ namespace osu.Game.Screens.Spectate } /// - /// Invoked when a spectated user's state has changed. + /// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing. /// /// The user whose state has changed. /// The new state. - protected abstract void OnUserStateChanged(int userId, [NotNull] SpectatorState spectatorState); + protected abstract void OnNewPlayingUserState(int userId, [NotNull] SpectatorState spectatorState); /// /// Starts gameplay for a user. From 1b8ada087d97ffdf8b62697a678ddaf8a8c4c654 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Feb 2022 19:25:51 +0800 Subject: [PATCH 476/587] Set `NoDefaultExcludes` to true This allows files such as .editorconfig and .gitignore to be included in the nupkg --- Templates/osu.Game.Templates.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj index 31a24a301f..4624d3d771 100644 --- a/Templates/osu.Game.Templates.csproj +++ b/Templates/osu.Game.Templates.csproj @@ -15,6 +15,7 @@ true false content + true From f1c6fdb2afce097de6a636cfff97aef2b337e70f Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Feb 2022 19:53:34 +0800 Subject: [PATCH 477/587] Update `.editorconfig`, `.gitignore` and DotSettings Basically just copied from root directory --- .../Rulesets/ruleset-empty/.editorconfig | 20 +-- Templates/Rulesets/ruleset-empty/.gitignore | 128 +++++++++++++----- ...ame.Rulesets.EmptyFreeform.sln.DotSettings | 83 +++++++++++- .../Rulesets/ruleset-example/.editorconfig | 20 +-- Templates/Rulesets/ruleset-example/.gitignore | 128 +++++++++++++----- ...osu.Game.Rulesets.Pippidon.sln.DotSettings | 83 +++++++++++- .../ruleset-scrolling-empty/.editorconfig | 20 +-- .../ruleset-scrolling-empty/.gitignore | 128 +++++++++++++----- ...me.Rulesets.EmptyScrolling.sln.DotSettings | 83 +++++++++++- .../ruleset-scrolling-example/.editorconfig | 20 +-- .../ruleset-scrolling-example/.gitignore | 128 +++++++++++++----- ...osu.Game.Rulesets.Pippidon.sln.DotSettings | 83 +++++++++++- 12 files changed, 696 insertions(+), 228 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/.editorconfig b/Templates/Rulesets/ruleset-empty/.editorconfig index f3badda9b3..b01749195f 100644 --- a/Templates/Rulesets/ruleset-empty/.editorconfig +++ b/Templates/Rulesets/ruleset-empty/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,6 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error + +dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-empty/.gitignore b/Templates/Rulesets/ruleset-empty/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-empty/.gitignore +++ b/Templates/Rulesets/ruleset-empty/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings index aa8f8739c1..2ff0f4d30b 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -909,26 +922,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-example/.editorconfig b/Templates/Rulesets/ruleset-example/.editorconfig index f3badda9b3..b01749195f 100644 --- a/Templates/Rulesets/ruleset-example/.editorconfig +++ b/Templates/Rulesets/ruleset-example/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,6 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error + +dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-example/.gitignore b/Templates/Rulesets/ruleset-example/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-example/.gitignore +++ b/Templates/Rulesets/ruleset-example/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings index aa8f8739c1..2ff0f4d30b 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -909,26 +922,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig index f3badda9b3..b01749195f 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig +++ b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,6 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error + +dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.gitignore b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/.gitignore +++ b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings index aa8f8739c1..2ff0f4d30b 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -909,26 +922,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig index f3badda9b3..b01749195f 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig +++ b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,6 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error + +dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-scrolling-example/.gitignore b/Templates/Rulesets/ruleset-scrolling-example/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/.gitignore +++ b/Templates/Rulesets/ruleset-scrolling-example/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings index aa8f8739c1..2ff0f4d30b 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -909,26 +922,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True From d06d584867e57ee6cb03cc566c1114ada0f47473 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Feb 2022 19:56:32 +0800 Subject: [PATCH 478/587] Change assembly titles So that they match the source name specified in template.json and can get replaced --- .../osu.Game.Rulesets.EmptyFreeform.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.csproj | 2 +- .../osu.Game.Rulesets.EmptyScrolling.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj index cfe2bd1cb2..092a013614 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.EmptyFreeform Library AnyCPU osu.Game.Rulesets.EmptyFreeform diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj index 61b859f45b..a3607343c9 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.Pippidon Library AnyCPU osu.Game.Rulesets.Pippidon diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj index 9dce3c9a0a..2ea52429ab 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.EmptyScrolling Library AnyCPU osu.Game.Rulesets.EmptyScrolling diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj index 61b859f45b..a3607343c9 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.Pippidon Library AnyCPU osu.Game.Rulesets.Pippidon From 036d17d9fd215423f34ace116203af936ef7428e Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Feb 2022 20:48:14 +0800 Subject: [PATCH 479/587] Remove licence headers --- Templates/Rulesets/ruleset-empty/.editorconfig | 2 -- .../osu.Game.Rulesets.EmptyFreeform.sln.DotSettings | 3 --- Templates/Rulesets/ruleset-example/.editorconfig | 2 -- .../ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings | 3 --- Templates/Rulesets/ruleset-scrolling-empty/.editorconfig | 2 -- .../osu.Game.Rulesets.EmptyScrolling.sln.DotSettings | 3 --- Templates/Rulesets/ruleset-scrolling-example/.editorconfig | 2 -- .../osu.Game.Rulesets.Pippidon.sln.DotSettings | 3 --- 8 files changed, 20 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/.editorconfig b/Templates/Rulesets/ruleset-empty/.editorconfig index b01749195f..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-empty/.editorconfig +++ b/Templates/Rulesets/ruleset-empty/.editorconfig @@ -190,5 +190,3 @@ dotnet_diagnostic.CA2225.severity = none # Banned APIs dotnet_diagnostic.RS0030.severity = error - -dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings index 2ff0f4d30b..9752e08599 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings @@ -730,9 +730,6 @@ </Group> </TypePattern> </Patterns> - Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. -See the LICENCE file in the repository root for full licence text. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> diff --git a/Templates/Rulesets/ruleset-example/.editorconfig b/Templates/Rulesets/ruleset-example/.editorconfig index b01749195f..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-example/.editorconfig +++ b/Templates/Rulesets/ruleset-example/.editorconfig @@ -190,5 +190,3 @@ dotnet_diagnostic.CA2225.severity = none # Banned APIs dotnet_diagnostic.RS0030.severity = error - -dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings index 2ff0f4d30b..9752e08599 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -730,9 +730,6 @@ </Group> </TypePattern> </Patterns> - Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. -See the LICENCE file in the repository root for full licence text. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig index b01749195f..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig +++ b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig @@ -190,5 +190,3 @@ dotnet_diagnostic.CA2225.severity = none # Banned APIs dotnet_diagnostic.RS0030.severity = error - -dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings index 2ff0f4d30b..9752e08599 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings @@ -730,9 +730,6 @@ </Group> </TypePattern> </Patterns> - Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. -See the LICENCE file in the repository root for full licence text. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> diff --git a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig index b01749195f..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig +++ b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig @@ -190,5 +190,3 @@ dotnet_diagnostic.CA2225.severity = none # Banned APIs dotnet_diagnostic.RS0030.severity = error - -dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings index 2ff0f4d30b..9752e08599 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -730,9 +730,6 @@ </Group> </TypePattern> </Patterns> - Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. -See the LICENCE file in the repository root for full licence text. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> From f47748591a2deb2604f9556bfbe4bdad754ba96a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Feb 2022 12:11:26 +0900 Subject: [PATCH 480/587] Update fastlane to latest release This pulls in the fix from souyuz to allow us to bring things up-to-date again (see https://github.com/voydz/souyuz/pull/36#event-6033249116). Have tested builds locally to work as expected. --- Gemfile.lock | 71 ++++++++++++++++++------------------ fastlane/README.md | 91 +++++++++++++++++++++++++++++++--------------- 2 files changed, 96 insertions(+), 66 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 86c8baabe6..1010027af9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,17 +8,17 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.551.0) - aws-sdk-core (3.125.5) + aws-partitions (1.553.0) + aws-sdk-core (3.126.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.53.0) - aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-kms (1.54.0) + aws-sdk-core (~> 3, >= 3.126.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.111.3) - aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-s3 (1.112.0) + aws-sdk-core (~> 3, >= 3.126.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) @@ -27,8 +27,8 @@ GEM claide (1.1.0) colored (1.2) colored2 (3.1.2) - commander-fastlane (4.4.6) - highline (~> 1.7.2) + commander (4.6.0) + highline (~> 2.0.0) declarative (0.0.20) digest-crc (0.6.4) rake (>= 12.0.0, < 14.0.0) @@ -36,7 +36,7 @@ GEM unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) emoji_regex (3.2.3) - excon (0.90.0) + excon (0.91.0) faraday (1.9.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -66,15 +66,15 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.181.0) + fastlane (2.204.2) CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.3, < 3.0.0) + addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) colored - commander-fastlane (>= 4.4.6, < 5.0.0) + commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) excon (>= 0.71.0, < 1.0.0) @@ -83,19 +83,20 @@ GEM faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-api-client (>= 0.37.0, < 0.39.0) - google-cloud-storage (>= 1.15.0, < 2.0.0) - highline (>= 1.7.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (~> 2.0.0) naturally (~> 2.2) + optparse (~> 0.1.1) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) simctl (~> 1.6.3) - slack-notifier (>= 2.0.0, < 3.0.0) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (>= 1.4.5, < 2.0.0) tty-screen (>= 0.6.3, < 1.0.0) @@ -105,18 +106,12 @@ GEM xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) fastlane-plugin-clean_testflight_testers (0.3.0) - fastlane-plugin-souyuz (0.9.1) - souyuz (= 0.9.1) + fastlane-plugin-souyuz (0.11.1) + souyuz (= 0.11.1) fastlane-plugin-xamarin (0.6.3) gh_inspector (1.1.3) - google-api-client (0.38.0) - addressable (~> 2.5, >= 2.5.1) - googleauth (~> 0.9) - httpclient (>= 2.8.1, < 3.0) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.0) - signet (~> 0.12) + google-apis-androidpublisher_v3 (0.16.0) + google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.2) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) @@ -128,6 +123,8 @@ GEM webrick google-apis-iamcredentials_v1 (0.10.0) google-apis-core (>= 0.4, < 2.a) + google-apis-playcustomapp_v1 (0.7.0) + google-apis-core (>= 0.4, < 2.a) google-apis-storage_v1 (0.11.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) @@ -144,14 +141,14 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (0.17.1) + googleauth (1.1.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.15) - highline (1.7.10) + signet (>= 0.16, < 2.a) + highline (2.0.3) http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) @@ -161,16 +158,19 @@ GEM memoist (0.16.2) mini_magick (4.11.0) mini_mime (1.1.2) - mini_portile2 (2.4.0) + mini_portile2 (2.7.1) multi_json (1.15.0) multipart-post (2.0.0) nanaimo (0.3.0) naturally (2.2.1) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) + nokogiri (1.13.1) + mini_portile2 (~> 2.7.0) + racc (~> 1.4) + optparse (0.1.1) os (1.1.4) plist (3.6.0) public_suffix (4.0.6) + racc (1.6.0) rake (13.0.6) representable (3.1.1) declarative (< 0.1.0) @@ -190,10 +190,9 @@ GEM simctl (1.6.8) CFPropertyList naturally - slack-notifier (2.4.0) - souyuz (0.9.1) - fastlane (>= 1.103.0) - highline (~> 1.7) + souyuz (0.11.1) + fastlane (>= 2.182.0) + highline (~> 2.0) nokogiri (~> 1.7) terminal-notifier (2.0.0) terminal-table (1.8.0) diff --git a/fastlane/README.md b/fastlane/README.md index 8273fdaa5d..9d5e11f7cb 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -1,78 +1,109 @@ fastlane documentation -================ +---- + # Installation Make sure you have the latest version of the Xcode command line tools installed: -``` +```sh xcode-select --install ``` -Install _fastlane_ using -``` -[sudo] gem install fastlane -NV -``` -or alternatively using `brew install fastlane` +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) # Available Actions + ## Android + ### android beta + +```sh +[bundle exec] fastlane android beta ``` -fastlane android beta -``` + Deploy to play store + ### android build_github + +```sh +[bundle exec] fastlane android build_github ``` -fastlane android build_github -``` + Deploy to github release + ### android build + +```sh +[bundle exec] fastlane android build ``` -fastlane android build -``` + Compile the project + ### android update_version + +```sh +[bundle exec] fastlane android update_version ``` -fastlane android update_version -``` + ---- + ## iOS + ### ios beta + +```sh +[bundle exec] fastlane ios beta ``` -fastlane ios beta -``` + Deploy to testflight + ### ios build + +```sh +[bundle exec] fastlane ios build ``` -fastlane ios build -``` + Compile the project + ### ios provision + +```sh +[bundle exec] fastlane ios provision ``` -fastlane ios provision -``` + Install provisioning profiles using match + ### ios update_version + +```sh +[bundle exec] fastlane ios update_version ``` -fastlane ios update_version -``` + + ### ios testflight_prune_dry -``` -fastlane ios testflight_prune_dry + +```sh +[bundle exec] fastlane ios testflight_prune_dry ``` + + ### ios testflight_prune + +```sh +[bundle exec] fastlane ios testflight_prune ``` -fastlane ios testflight_prune -``` + ---- -This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. -More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). -The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). From a3896a8ebdc87b95c9d97a1f5112c65bafe761b9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Feb 2022 14:18:29 +0900 Subject: [PATCH 481/587] Remove allowance of null dependency --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 277040b2a6..0a94b55221 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI public int RecordFrameRate = 60; - [Resolved(canBeNull: true)] + [Resolved] private SpectatorClient spectatorClient { get; set; } [Resolved] @@ -48,8 +48,7 @@ namespace osu.Game.Rulesets.UI base.LoadComplete(); inputManager = GetContainingInputManager(); - - spectatorClient?.BeginPlaying(gameplayState, target); + spectatorClient.BeginPlaying(gameplayState, target); } protected override void Dispose(bool isDisposing) From f7fb7825cc322c3ac08a0c310928591ed96e3142 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Feb 2022 14:21:33 +0900 Subject: [PATCH 482/587] Simplify disposal --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 0a94b55221..6843beef7b 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -54,9 +55,7 @@ namespace osu.Game.Rulesets.UI protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - - if (spectatorClient != null && gameplayState != null) - spectatorClient.EndPlaying(gameplayState); + spectatorClient?.EndPlaying(gameplayState); } protected override void Update() From ebd105422fd6c2903bced01cb820d7e0d542789a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Feb 2022 14:22:08 +0900 Subject: [PATCH 483/587] Remove unused using --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 6843beef7b..dcd8f12028 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; From 88bb9d4237c62d37678d8e0e110438ea247b7ae5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Feb 2022 15:50:03 +0900 Subject: [PATCH 484/587] Fix migration errors not outputting the call stack to logs --- .../Settings/Sections/Maintenance/MigrationRunScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index b0b61554eb..adb347e7b8 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance .ContinueWith(t => { if (t.IsFaulted) - Logger.Log($"Error during migration: {t.Exception?.Message}", level: LogLevel.Error); + Logger.Error(t.Exception, $"Error during migration: {t.Exception?.Message}"); Schedule(this.Exit); }); From 176bb4a4e22901a37761ab6e1812663d3ec6c7dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Feb 2022 15:25:54 +0900 Subject: [PATCH 485/587] Update desktop projects to target .NET 6 --- .run/osu! (Second Client).run.xml | 8 ++++---- .../osu.Game.Rulesets.EmptyFreeform.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.EmptyScrolling.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 4 ++-- osu.Desktop/osu.Desktop.csproj | 2 +- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Mania.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Osu.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Taiko.Tests.csproj | 4 ++-- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- .../osu.Game.Tournament.Tests.csproj | 4 ++-- osu.iOS.props | 2 +- 14 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.run/osu! (Second Client).run.xml b/.run/osu! (Second Client).run.xml index 599b4b986b..9a471df902 100644 --- a/.run/osu! (Second Client).run.xml +++ b/.run/osu! (Second Client).run.xml @@ -1,8 +1,8 @@ - - \ No newline at end of file + diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index 3c6aaa39ca..cb922c5a58 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -20,7 +20,7 @@ WinExe - net5.0 + net6.0 osu.Game.Rulesets.EmptyFreeform.Tests - \ No newline at end of file + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 0719dd30df..5ecd9cc675 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -20,7 +20,7 @@ WinExe - net5.0 + net6.0 osu.Game.Rulesets.Pippidon.Tests - \ No newline at end of file + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index d0db43cc81..33ad0ac4f7 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -20,7 +20,7 @@ WinExe - net5.0 + net6.0 osu.Game.Rulesets.EmptyScrolling.Tests - \ No newline at end of file + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 0719dd30df..5ecd9cc675 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -20,7 +20,7 @@ WinExe - net5.0 + net6.0 osu.Game.Rulesets.Pippidon.Tests - \ No newline at end of file + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 89b9ffb94b..5e203af1f2 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -1,6 +1,6 @@  - net5.0 + net6.0 WinExe true A free-to-win rhythm game. Rhythm is just a *click* away! diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 57b914bee6..434c0e0367 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 Exe false diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 13f2e25f05..fc6d900567 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -9,9 +9,9 @@ WinExe - net5.0 + net6.0 - \ No newline at end of file + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index d51a6da4f9..ddad2adfea 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -9,9 +9,9 @@ WinExe - net5.0 + net6.0 - \ No newline at end of file + diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index fea2e408f6..bd4c3d3345 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -10,9 +10,9 @@ WinExe - net5.0 + net6.0 - \ No newline at end of file + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index ad3713e047..a6b8eb8651 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -9,9 +9,9 @@ WinExe - net5.0 + net6.0 - \ No newline at end of file + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 3b115d43e5..acf1e8470b 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -12,7 +12,7 @@ WinExe - net5.0 + net6.0 tests.ruleset diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 130fcfaca1..c7314a4969 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -11,7 +11,7 @@ WinExe - net5.0 + net6.0 @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/osu.iOS.props b/osu.iOS.props index 5978f6d685..7e5ab37257 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -63,7 +63,7 @@ - + $(NoWarn);NU1605 From 44f2d8a4481d686aea94d6d480cfc0e17ec7351b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Feb 2022 18:48:37 +0900 Subject: [PATCH 486/587] Allow game folder migration to fail gracefully when cleanup cannot completely succeed --- osu.Game.Tournament/IO/TournamentStorage.cs | 4 ++- osu.Game/IO/MigratableStorage.cs | 30 +++++++++++----- osu.Game/IO/OsuStorage.cs | 7 ++-- osu.Game/OsuGameBase.cs | 9 ++--- .../Maintenance/MigrationRunScreen.cs | 36 ++++++++++++++++--- 5 files changed, 66 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 347d368a04..b4859d0c91 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tournament.IO public IEnumerable ListTournaments() => AllTournaments.GetDirectories(string.Empty); - public override void Migrate(Storage newStorage) + public override bool Migrate(Storage newStorage) { // this migration only happens once on moving to the per-tournament storage system. // listed files are those known at that point in time. @@ -94,6 +94,8 @@ namespace osu.Game.Tournament.IO ChangeTargetStorage(newStorage); storageConfig.SetValue(StorageConfig.CurrentTournament, default_tournament); storageConfig.Save(); + + return true; } private void moveFileIfExists(string file, DirectoryInfo destination) diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 1b76725b04..e478144294 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -33,7 +33,8 @@ namespace osu.Game.IO /// A general purpose migration method to move the storage to a different location. /// The target storage of the migration. /// - public virtual void Migrate(Storage newStorage) + /// Whether cleanup could complete. + public virtual bool Migrate(Storage newStorage) { var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newStorage.GetFullPath(".")); @@ -57,17 +58,20 @@ namespace osu.Game.IO CopyRecursive(source, destination); ChangeTargetStorage(newStorage); - DeleteRecursive(source); + + return DeleteRecursive(source); } - protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) + protected bool DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) { + bool allFilesDeleted = true; + foreach (System.IO.FileInfo fi in target.GetFiles()) { if (topLevelExcludes && IgnoreFiles.Contains(fi.Name)) continue; - AttemptOperation(() => fi.Delete()); + allFilesDeleted &= AttemptOperation(() => fi.Delete(), throwOnFailure: false); } foreach (DirectoryInfo dir in target.GetDirectories()) @@ -75,11 +79,13 @@ namespace osu.Game.IO if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name)) continue; - AttemptOperation(() => dir.Delete(true)); + allFilesDeleted &= AttemptOperation(() => dir.Delete(true), throwOnFailure: false); } if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) - AttemptOperation(target.Delete); + allFilesDeleted &= AttemptOperation(target.Delete, throwOnFailure: false); + + return allFilesDeleted; } protected void CopyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) @@ -110,19 +116,25 @@ namespace osu.Game.IO /// /// The action to perform. /// The number of attempts (250ms wait between each). - protected static void AttemptOperation(Action action, int attempts = 10) + /// Whether to throw an exception on failure. If false, will silently fail. + protected static bool AttemptOperation(Action action, int attempts = 10, bool throwOnFailure = true) { while (true) { try { action(); - return; + return true; } catch (Exception) { if (attempts-- == 0) - throw; + { + if (throwOnFailure) + throw; + + return false; + } } Thread.Sleep(250); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 802c71e363..6e7cb545e3 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -113,11 +113,14 @@ namespace osu.Game.IO } } - public override void Migrate(Storage newStorage) + public override bool Migrate(Storage newStorage) { - base.Migrate(newStorage); + bool cleanupSucceeded = base.Migrate(newStorage); + storageConfig.SetValue(StorageConfig.FullPath, newStorage.GetFullPath(".")); storageConfig.Save(); + + return cleanupSucceeded; } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5b2eb5607a..0b2644d5ba 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -413,7 +413,7 @@ namespace osu.Game Scheduler.AddDelayed(GracefullyExit, 2000); } - public void Migrate(string path) + public bool Migrate(string path) { Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""..."); @@ -432,14 +432,15 @@ namespace osu.Game readyToRun.Wait(); - (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + + Logger.Log(@"Migration complete!"); + return cleanupSucceded != false; } finally { realmBlocker?.Dispose(); } - - Logger.Log(@"Migration complete!"); } protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index adb347e7b8..fb7ff0dbd1 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -4,13 +4,16 @@ using System.IO; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osuTK; @@ -23,6 +26,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance [Resolved(canBeNull: true)] private OsuGame game { get; set; } + [Resolved] + private NotificationOverlay notifications { get; set; } + + [Resolved] + private Storage storage { get; set; } + + [Resolved] + private GameHost host { get; set; } + public override bool AllowBackButton => false; public override bool AllowExternalScreenChange => false; @@ -84,17 +96,33 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Beatmap.Value = Beatmap.Default; + var originalStorage = new NativeStorage(storage.GetFullPath(string.Empty), host); + migrationTask = Task.Run(PerformMigration) - .ContinueWith(t => + .ContinueWith(task => { - if (t.IsFaulted) - Logger.Error(t.Exception, $"Error during migration: {t.Exception?.Message}"); + if (task.IsFaulted) + { + Logger.Error(task.Exception, $"Error during migration: {task.Exception?.Message}"); + } + else if (!task.GetResultSafely()) + { + notifications.Post(new SimpleNotification + { + Text = "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.", + Activated = () => + { + originalStorage.PresentExternally(); + return true; + } + }); + } Schedule(this.Exit); }); } - protected virtual void PerformMigration() => game?.Migrate(destination.FullName); + protected virtual bool PerformMigration() => game?.Migrate(destination.FullName) != false; public override void OnEntering(IScreen last) { From 19cb8cb03a100c9baa80c23b7b307ff300130df2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Feb 2022 19:21:17 +0900 Subject: [PATCH 487/587] Update tests --- .../Settings/TestSceneMigrationScreens.cs | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs index 2883e54385..a68090504d 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs @@ -3,32 +3,69 @@ using System.IO; using System.Threading; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections.Maintenance; namespace osu.Game.Tests.Visual.Settings { public class TestSceneMigrationScreens : ScreenTestScene { + [Cached] + private readonly NotificationOverlay notifications; + public TestSceneMigrationScreens() { - AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen())); + Children = new Drawable[] + { + notifications = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + }; + } + + [Test] + public void TestDeleteSuccess() + { + AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen(true))); + } + + [Test] + public void TestDeleteFails() + { + AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen(false))); } private class TestMigrationSelectScreen : MigrationSelectScreen { - protected override void BeginMigration(DirectoryInfo target) => this.Push(new TestMigrationRunScreen()); + private readonly bool deleteSuccess; + + public TestMigrationSelectScreen(bool deleteSuccess) + { + this.deleteSuccess = deleteSuccess; + } + + protected override void BeginMigration(DirectoryInfo target) => this.Push(new TestMigrationRunScreen(deleteSuccess)); private class TestMigrationRunScreen : MigrationRunScreen { - protected override void PerformMigration() - { - Thread.Sleep(3000); - } + private readonly bool success; - public TestMigrationRunScreen() + public TestMigrationRunScreen(bool success) : base(null) { + this.success = success; + } + + protected override bool PerformMigration() + { + Thread.Sleep(3000); + return success; } } } From 2939bc4644b2b5079a5af5ffa578288676cf0ca2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 01:49:52 +0900 Subject: [PATCH 488/587] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 71525a7acb..147f576c55 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c8f634284b..dd10807ec2 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 5978f6d685..6fbc468586 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + From 9cd88ec2b84d8460af461bd91deec5fc355ddced Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Feb 2022 21:23:38 +0300 Subject: [PATCH 489/587] Update API models with score pinning changes --- osu.Game/Online/API/Requests/GetUserScoresRequest.cs | 3 ++- osu.Game/Online/API/Requests/Responses/APIUser.cs | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs index 653abf7427..5d39799f6b 100644 --- a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs @@ -39,6 +39,7 @@ namespace osu.Game.Online.API.Requests { Best, Firsts, - Recent + Recent, + Pinned } } diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index e4a432b074..2b64e5de06 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -151,6 +151,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"scores_recent_count")] public int ScoresRecentCount; + [JsonProperty(@"scores_pinned_count")] + public int ScoresPinnedCount; + [JsonProperty(@"beatmap_playcounts_count")] public int BeatmapPlayCountsCount; From 4f7003928acb9a14a12adc02c343cb9329181ec5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Feb 2022 21:32:07 +0300 Subject: [PATCH 490/587] Add score container for pinned scores in ranks section --- .../Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs | 3 +++ osu.Game/Overlays/Profile/Sections/RanksSection.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 5532e35cc5..5c67da1911 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -46,6 +46,9 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks case ScoreType.Recent: return user.ScoresRecentCount; + case ScoreType.Pinned: + return user.ScoresPinnedCount; + default: return 0; } diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 00a68d5bf9..02d8bd8c52 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -18,6 +18,8 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { + // todo: update to use UsersStrings.ShowExtraTopRanksPinnedTitle once that exists. + new PaginatedScoreContainer(ScoreType.Pinned, User, "Pinned Scores"), new PaginatedScoreContainer(ScoreType.Best, User, UsersStrings.ShowExtraTopRanksBestTitle), new PaginatedScoreContainer(ScoreType.Firsts, User, UsersStrings.ShowExtraTopRanksFirstTitle) }; From 9574bc13820665f5e6721915b3c05367ac9dccb7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:00:40 +0300 Subject: [PATCH 491/587] Allow `IRulesetInfo`s of same type to be comparable At first I was planning on making `CompareTo` implemented at `IRulesetInfo` itself and shared across classes, but turns out it only implements it explicitly and not allow direct `IRulesetInfo.Equals` calls. It messed with my head enough that I decided to just let each class have its own implementation and only allow same type. --- osu.Game/Rulesets/IRulesetInfo.cs | 2 +- osu.Game/Rulesets/RulesetInfo.cs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index 44731a2495..60a02212fc 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets /// /// A representation of a ruleset's metadata. /// - public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable + public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable { /// /// The user-exposed name of this ruleset. diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 0a0941d1ff..88e3988431 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] [MapTo("Ruleset")] - public class RulesetInfo : RealmObject, IEquatable, IRulesetInfo + public class RulesetInfo : RealmObject, IEquatable, IComparable, IRulesetInfo { [PrimaryKey] public string ShortName { get; set; } = string.Empty; @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets return ShortName == other.ShortName; } - public bool Equals(IRulesetInfo? other) => other is RulesetInfo b && Equals(b); + public bool Equals(IRulesetInfo? other) => other is RulesetInfo r && Equals(r); public int CompareTo(RulesetInfo other) { @@ -63,6 +63,14 @@ namespace osu.Game.Rulesets return string.Compare(ShortName, other.ShortName, StringComparison.Ordinal); } + public int CompareTo(IRulesetInfo other) + { + if (!(other is RulesetInfo ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(RulesetInfo)}.", nameof(other)); + + return CompareTo(ruleset); + } + public override int GetHashCode() { // Importantly, ignore the underlying realm hash code, as it will usually not match. From 1b729e891d53e8134efcd5ef8f09d5655c5bf20f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:01:10 +0300 Subject: [PATCH 492/587] Update pointless `CompareTo` implementation once again --- osu.Game/Rulesets/EFRulesetInfo.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs index ba56adac49..4174aa773c 100644 --- a/osu.Game/Rulesets/EFRulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] [Table(@"RulesetInfo")] - public sealed class EFRulesetInfo : IEquatable, IRulesetInfo + public sealed class EFRulesetInfo : IEquatable, IComparable, IRulesetInfo { public int? ID { get; set; } @@ -42,7 +42,15 @@ namespace osu.Game.Rulesets public bool Equals(EFRulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; - public int CompareTo(RulesetInfo other) => OnlineID.CompareTo(other.OnlineID); + public int CompareTo(EFRulesetInfo other) => OnlineID.CompareTo(other.OnlineID); + + public int CompareTo(IRulesetInfo other) + { + if (!(other is EFRulesetInfo ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(EFRulesetInfo)}.", nameof(other)); + + return CompareTo(ruleset); + } public override bool Equals(object obj) => obj is EFRulesetInfo rulesetInfo && Equals(rulesetInfo); From 26839f6ad8f3c49c3d1c63016b3af47e9afd2602 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:17:24 +0300 Subject: [PATCH 493/587] Consider `OnlineID`s during ruleset equality if available Required for `APIBeatmap`s, which provide `Ruleset` instances with `OnlineID` available only. Also consistent with the comparer implementation. --- osu.Game/Rulesets/RulesetInfo.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 88e3988431..ba7c8d191d 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -44,6 +44,9 @@ namespace osu.Game.Rulesets if (ReferenceEquals(this, other)) return true; if (other == null) return false; + if (OnlineID >= 0 && other.OnlineID >= 0) + return OnlineID == other.OnlineID; + return ShortName == other.ShortName; } From 6f0e32826c6d90c92c1130cd7aa07894e3b7f38f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:06:28 +0300 Subject: [PATCH 494/587] Standardise ordering/grouping of `IRulesetInfo`/`RulesetInfo`s --- .../Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs | 2 +- osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs | 6 ++---- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Select/Carousel/SetPanelContent.cs | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs index 7753d8480a..eeb86f4702 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs @@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards bool firstGroup = true; - foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key)) + foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) { if (!firstGroup) { diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 5b211084ab..5b467d67e2 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -62,10 +62,8 @@ namespace osu.Game.Beatmaps.Drawables // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 bool collapsed = beatmapSet.Beatmaps.Count() > 12; - foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key)) - { - flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key, rulesetGrouping, collapsed)); - } + foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) + flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed)); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 5503a62ba2..2aec63fa65 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -851,7 +851,7 @@ namespace osu.Game.Screens.Edit var difficultyItems = new List(); - foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset.ShortName).OrderBy(group => group.Key)) + foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset).OrderBy(group => group.Key)) { if (difficultyItems.Count > 0) difficultyItems.Add(new EditorMenuItemSpacer()); diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 82523c9d9d..760915b528 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Select.Carousel var beatmaps = carouselSet.Beatmaps.ToList(); return beatmaps.Count > maximum_difficulty_icons - ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset.ShortName) + ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset) .Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset)) : beatmaps.Select(b => new FilterableDifficultyIcon(b)); } From c29cc78853f9cfb5efc4e923c7d9fca6fda8fbf7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:44:54 +0300 Subject: [PATCH 495/587] Fix failing test case --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index b429619044..9083415a78 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -457,10 +457,12 @@ namespace osu.Game.Tests.Visual.UserInterface public override ModType Type => ModType.Conversion; } - private class TestUnimplementedModOsuRuleset : OsuRuleset + private class TestUnimplementedModOsuRuleset : OsuRuleset, ILegacyRuleset { public override string ShortName => "unimplemented"; + int ILegacyRuleset.LegacyID => -1; + public override IEnumerable GetModsFor(ModType type) { if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); From 92e22c57a77a2860e2ddc78ee85da46f2e5f43d4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 08:02:51 +0300 Subject: [PATCH 496/587] Introduce private `APIRuleset` for online ID equality comparison --- .../API/Requests/Responses/APIBeatmap.cs | 26 ++++++++++++++++++- osu.Game/Rulesets/RulesetInfo.cs | 3 --- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index ebbac0dcab..dca60e54cb 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -98,7 +98,7 @@ namespace osu.Game.Online.API.Requests.Responses public string MD5Hash => Checksum; - public IRulesetInfo Ruleset => new RulesetInfo { OnlineID = RulesetID }; + public IRulesetInfo Ruleset => new APIRuleset { OnlineID = RulesetID }; [JsonIgnore] public string Hash => throw new NotImplementedException(); @@ -106,5 +106,29 @@ namespace osu.Game.Online.API.Requests.Responses #endregion public bool Equals(IBeatmapInfo? other) => other is APIBeatmap b && this.MatchesOnlineID(b); + + private class APIRuleset : IRulesetInfo + { + public int OnlineID { get; set; } = -1; + + public string Name => $@"{nameof(APIRuleset)} (ID: {OnlineID})"; + public string ShortName => nameof(APIRuleset); + public string InstantiationInfo => string.Empty; + + public Ruleset CreateInstance() => throw new NotImplementedException(); + + public bool Equals(IRulesetInfo? other) => other is APIRuleset r && this.MatchesOnlineID(r); + + public int CompareTo(IRulesetInfo other) + { + if (!(other is APIRuleset ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(APIRuleset)}.", nameof(other)); + + return OnlineID.CompareTo(ruleset.OnlineID); + } + + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => OnlineID; + } } } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index ba7c8d191d..88e3988431 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -44,9 +44,6 @@ namespace osu.Game.Rulesets if (ReferenceEquals(this, other)) return true; if (other == null) return false; - if (OnlineID >= 0 && other.OnlineID >= 0) - return OnlineID == other.OnlineID; - return ShortName == other.ShortName; } From b06caf2bf7b3a8705d26101f5e1fba464542a701 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 09:12:02 +0300 Subject: [PATCH 497/587] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 147f576c55..1a2859c851 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index dd10807ec2..a9c0226951 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6fbc468586..5e0b264834 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + From f049f175d5d2d3a971bce6825d8cdbd2fedea42d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 15:19:55 +0900 Subject: [PATCH 498/587] Revert "Fix failing test case" This reverts commit c29cc78853f9cfb5efc4e923c7d9fca6fda8fbf7. --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 9083415a78..b429619044 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -457,12 +457,10 @@ namespace osu.Game.Tests.Visual.UserInterface public override ModType Type => ModType.Conversion; } - private class TestUnimplementedModOsuRuleset : OsuRuleset, ILegacyRuleset + private class TestUnimplementedModOsuRuleset : OsuRuleset { public override string ShortName => "unimplemented"; - int ILegacyRuleset.LegacyID => -1; - public override IEnumerable GetModsFor(ModType type) { if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); From f012f64fd1f441c96c589035afd63010b41d0b1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 16:45:57 +0900 Subject: [PATCH 499/587] Add test coverage checking carousel panel visual state after ruleset filter change --- .../SongSelect/TestSceneBeatmapCarousel.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 4e46901e08..540b820250 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -41,6 +41,68 @@ namespace osu.Game.Tests.Visual.SongSelect this.rulesets = rulesets; } + [Test] + public void TestExternalRulesetChange() + { + createCarousel(new List()); + + AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(0), + AllowConvertedBeatmaps = true, + }, false)); + + AddStep("add mixed ruleset beatmapset", () => + { + var testMixed = TestResources.CreateTestBeatmapSetInfo(3); + + for (int i = 0; i <= 2; i++) + { + testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i); + } + + carousel.UpdateBeatmapSet(testMixed); + }); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1; + }); + + AddStep("filter to ruleset 1", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(1), + AllowConvertedBeatmaps = true, + }, false)); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 2 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 1) == 1; + }); + + AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(2), + AllowConvertedBeatmaps = true, + }, false)); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 2 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 2) == 1; + }); + } + [Test] public void TestScrollPositionMaintainedOnAdd() { From ccd664896185a0aff852fb8eaf6c47cb1b7bf125 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 09:22:09 +0300 Subject: [PATCH 500/587] Update pinned score container header to use localised title --- osu.Game/Overlays/Profile/Sections/RanksSection.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 02d8bd8c52..f48e33dc12 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -18,8 +18,7 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - // todo: update to use UsersStrings.ShowExtraTopRanksPinnedTitle once that exists. - new PaginatedScoreContainer(ScoreType.Pinned, User, "Pinned Scores"), + new PaginatedScoreContainer(ScoreType.Pinned, User, UsersStrings.ShowExtraTopRanksPinnedTitle), new PaginatedScoreContainer(ScoreType.Best, User, UsersStrings.ShowExtraTopRanksBestTitle), new PaginatedScoreContainer(ScoreType.Firsts, User, UsersStrings.ShowExtraTopRanksFirstTitle) }; From beb3731c0b079c6b2bdaf1d051bd51b5350899f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 15:53:45 +0900 Subject: [PATCH 501/587] Standardise and combine base implementation of score submission requests These share too much yet have very different constructor signatures and property exposure. Just a clean-up pass as I begin to look at replay submission. --- .../Online/Rooms/SubmitRoomScoreRequest.cs | 32 ++------------- osu.Game/Online/Rooms/SubmitScoreRequest.cs | 41 +++++++++++++++++++ .../Online/Solo/SubmitSoloScoreRequest.cs | 33 ++------------- osu.Game/Screens/Play/RoomSubmittingPlayer.cs | 2 +- osu.Game/Screens/Play/SoloPlayer.cs | 2 +- 5 files changed, 51 insertions(+), 59 deletions(-) create mode 100644 osu.Game/Online/Rooms/SubmitScoreRequest.cs diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs index e24d113822..39193be1af 100644 --- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs @@ -1,46 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Net.Http; -using Newtonsoft.Json; -using osu.Framework.IO.Network; -using osu.Game.Online.API; -using osu.Game.Online.Solo; using osu.Game.Scoring; namespace osu.Game.Online.Rooms { - public class SubmitRoomScoreRequest : APIRequest + public class SubmitRoomScoreRequest : SubmitScoreRequest { - private readonly long scoreId; private readonly long roomId; private readonly long playlistItemId; - private readonly SubmittableScore score; - public SubmitRoomScoreRequest(long scoreId, long roomId, long playlistItemId, ScoreInfo scoreInfo) + public SubmitRoomScoreRequest(ScoreInfo scoreInfo, long scoreId, long roomId, long playlistItemId) + : base(scoreInfo, scoreId) { - this.scoreId = scoreId; this.roomId = roomId; this.playlistItemId = playlistItemId; - score = new SubmittableScore(scoreInfo); } - protected override WebRequest CreateWebRequest() - { - var req = base.CreateWebRequest(); - - req.ContentType = "application/json"; - req.Method = HttpMethod.Put; - req.Timeout = 30000; - - req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - })); - - return req; - } - - protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{scoreId}"; + protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{ScoreId}"; } } diff --git a/osu.Game/Online/Rooms/SubmitScoreRequest.cs b/osu.Game/Online/Rooms/SubmitScoreRequest.cs new file mode 100644 index 0000000000..14f858f007 --- /dev/null +++ b/osu.Game/Online/Rooms/SubmitScoreRequest.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using Newtonsoft.Json; +using osu.Framework.IO.Network; +using osu.Game.Online.API; +using osu.Game.Online.Solo; +using osu.Game.Scoring; + +namespace osu.Game.Online.Rooms +{ + public abstract class SubmitScoreRequest : APIRequest + { + public readonly SubmittableScore Score; + + protected readonly long ScoreId; + + protected SubmitScoreRequest(ScoreInfo scoreInfo, long scoreId) + { + Score = new SubmittableScore(scoreInfo); + this.ScoreId = scoreId; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.ContentType = "application/json"; + req.Method = HttpMethod.Put; + req.Timeout = 30000; + + req.AddRaw(JsonConvert.SerializeObject(Score, new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + })); + + return req; + } + } +} diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs index 78ebddb2e6..77fd7b813b 100644 --- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs @@ -1,46 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Net.Http; -using Newtonsoft.Json; -using osu.Framework.IO.Network; -using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; namespace osu.Game.Online.Solo { - public class SubmitSoloScoreRequest : APIRequest + public class SubmitSoloScoreRequest : SubmitScoreRequest { - public readonly SubmittableScore Score; - - private readonly long scoreId; - private readonly int beatmapId; - public SubmitSoloScoreRequest(int beatmapId, long scoreId, ScoreInfo scoreInfo) + public SubmitSoloScoreRequest(ScoreInfo scoreInfo, long scoreId, int beatmapId) + : base(scoreInfo, scoreId) { this.beatmapId = beatmapId; - this.scoreId = scoreId; - Score = new SubmittableScore(scoreInfo); } - protected override WebRequest CreateWebRequest() - { - var req = base.CreateWebRequest(); - - req.ContentType = "application/json"; - req.Method = HttpMethod.Put; - req.Timeout = 30000; - - req.AddRaw(JsonConvert.SerializeObject(Score, new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - })); - - return req; - } - - protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{scoreId}"; + protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{ScoreId}"; } } diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs index 1002e7607f..fc96dfa965 100644 --- a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs +++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play protected override APIRequest CreateSubmissionRequest(Score score, long token) { Debug.Assert(Room.RoomID.Value != null); - return new SubmitRoomScoreRequest(token, Room.RoomID.Value.Value, PlaylistItem.ID, score.ScoreInfo); + return new SubmitRoomScoreRequest(score.ScoreInfo, token, Room.RoomID.Value.Value, PlaylistItem.ID); } } } diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index eced2d142b..824c0072e3 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play Debug.Assert(beatmap.OnlineID > 0); - return new SubmitSoloScoreRequest(beatmap.OnlineID, token, score.ScoreInfo); + return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID); } } } From 908c31c68764370e44e9e3a6055aa881a7ce1971 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 16:02:25 +0900 Subject: [PATCH 502/587] Update stream read operations to use new helper methods --- osu.Game/Database/ImportTask.cs | 5 ++--- osu.Game/IO/Archives/ArchiveReader.cs | 14 +++----------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game/Database/ImportTask.cs b/osu.Game/Database/ImportTask.cs index cd9e396d13..d75c1a73e6 100644 --- a/osu.Game/Database/ImportTask.cs +++ b/osu.Game/Database/ImportTask.cs @@ -4,6 +4,7 @@ #nullable enable using System.IO; +using osu.Framework.Extensions; using osu.Game.IO.Archives; using osu.Game.Stores; using osu.Game.Utils; @@ -63,9 +64,7 @@ namespace osu.Game.Database if (!(stream is MemoryStream memoryStream)) { // This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out). - byte[] buffer = new byte[stream.Length]; - stream.Read(buffer, 0, (int)stream.Length); - memoryStream = new MemoryStream(buffer); + memoryStream = new MemoryStream(stream.ReadAllBytesToArray()); } if (ZipUtils.IsZipArchive(memoryStream)) diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs index 1d8da16c72..dab70eaf70 100644 --- a/osu.Game/IO/Archives/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using osu.Framework.Extensions; using osu.Framework.IO.Stores; namespace osu.Game.IO.Archives @@ -35,14 +36,7 @@ namespace osu.Game.IO.Archives public virtual byte[] Get(string name) { using (Stream input = GetStream(name)) - { - if (input == null) - return null; - - byte[] buffer = new byte[input.Length]; - input.Read(buffer); - return buffer; - } + return input?.ReadAllBytesToArray(); } public async Task GetAsync(string name, CancellationToken cancellationToken = default) @@ -52,9 +46,7 @@ namespace osu.Game.IO.Archives if (input == null) return null; - byte[] buffer = new byte[input.Length]; - await input.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - return buffer; + return await input.ReadAllBytesToArrayAsync(cancellationToken).ConfigureAwait(false); } } } From 6005daeba8bda373622123167bc533e2738704d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 16:02:42 +0900 Subject: [PATCH 503/587] Fix fire-and-forget async calls to use `WaitSafely` --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 8f6ba6375f..7661cf671b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -829,7 +829,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID - }))); + })).WaitSafely()); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); @@ -860,11 +860,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID - }))); + })).WaitSafely()); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); - AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2)); + AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2).WaitSafely()); AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1); AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); From 28bbf34b1495908b1e145f8575f6ffaaf0979bd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 16:21:49 +0900 Subject: [PATCH 504/587] Remove unnecessary `this.` prefix --- osu.Game/Online/Rooms/SubmitScoreRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/SubmitScoreRequest.cs b/osu.Game/Online/Rooms/SubmitScoreRequest.cs index 14f858f007..b263262d2b 100644 --- a/osu.Game/Online/Rooms/SubmitScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitScoreRequest.cs @@ -19,7 +19,7 @@ namespace osu.Game.Online.Rooms protected SubmitScoreRequest(ScoreInfo scoreInfo, long scoreId) { Score = new SubmittableScore(scoreInfo); - this.ScoreId = scoreId; + ScoreId = scoreId; } protected override WebRequest CreateWebRequest() From 2ed3d5853144436f76260feb38f0d4d1c3a56e52 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 12 Feb 2022 08:51:09 +0800 Subject: [PATCH 505/587] Ignore short spinners for relax mod --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 5d191119b9..905d55c64e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -87,8 +87,9 @@ namespace osu.Game.Rulesets.Osu.Mods requiresHold |= slider.Ball.IsHovered || h.IsHovered; break; - case DrawableSpinner _: - requiresHold = true; + case DrawableSpinner spinner: + if (spinner.HitObject.SpinsRequired > 0) + requiresHold = true; break; } } From 053f41d755999ec120c1674c958cfadadb7898f7 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 12 Feb 2022 10:06:43 +0800 Subject: [PATCH 506/587] Simplify code --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 905d55c64e..10abd24e80 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -88,8 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods break; case DrawableSpinner spinner: - if (spinner.HitObject.SpinsRequired > 0) - requiresHold = true; + requiresHold = spinner.HitObject.SpinsRequired > 0; break; } } From 639d813d06fe2275b6220fc41b6133e0390de5f5 Mon Sep 17 00:00:00 2001 From: PercyDan <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 12 Feb 2022 11:15:03 +0800 Subject: [PATCH 507/587] Don't override previous value Co-authored-by: Salman Ahmed --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 10abd24e80..1bf63ef6d4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods break; case DrawableSpinner spinner: - requiresHold = spinner.HitObject.SpinsRequired > 0; + requiresHold |= spinner.HitObject.SpinsRequired > 0; break; } } From a2c2b2bbb3dcbd87f9c4c08582b00b98f1096e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 17:52:59 +0100 Subject: [PATCH 508/587] Add flow for copying existing difficulty content --- osu.Game/Beatmaps/BeatmapManager.cs | 34 ++++++++------ .../Screens/Edit/CreateNewDifficultyDialog.cs | 45 +++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 20 ++++++++- osu.Game/Screens/Edit/EditorLoader.cs | 7 ++- .../Edit/NewDifficultyCreationParameters.cs | 36 +++++++++++++++ osu.Game/Tests/Visual/EditorTestScene.cs | 2 +- 6 files changed, 125 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs create mode 100644 osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 633eb8f15e..bd9cdba9fb 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -20,6 +20,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; +using osu.Game.Screens.Edit; using osu.Game.Skinning; using osu.Game.Stores; @@ -112,29 +113,36 @@ namespace osu.Game.Beatmaps /// The new difficulty will be backed by a model /// and represented by the returned . /// - public virtual WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + public virtual WorkingBeatmap CreateNewBlankDifficulty(NewDifficultyCreationParameters creationParameters) { - // fetch one of the existing difficulties to copy timing points and metadata from, - // so that the user doesn't have to fill all of that out again. - // this silently assumes that all difficulties have the same timing points and metadata, - // but cases where this isn't true seem rather rare / pathological. - var referenceBeatmap = GetWorkingBeatmap(beatmapSetInfo.Beatmaps.First()); + var referenceBeatmap = creationParameters.ReferenceBeatmap; + var targetBeatmapSet = creationParameters.BeatmapSet; - var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); + var newBeatmapInfo = new BeatmapInfo(creationParameters.Ruleset, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); // populate circular beatmap set info <-> beatmap info references manually. // several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()` // rely on them being freely traversable in both directions for correct operation. - beatmapSetInfo.Beatmaps.Add(newBeatmapInfo); - newBeatmapInfo.BeatmapSet = beatmapSetInfo; + targetBeatmapSet.Beatmaps.Add(newBeatmapInfo); + newBeatmapInfo.BeatmapSet = targetBeatmapSet; - var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; - foreach (var timingPoint in referenceBeatmap.Beatmap.ControlPointInfo.TimingPoints) - newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); + IBeatmap newBeatmap; + + if (creationParameters.ClearAllObjects) + { + newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; + foreach (var timingPoint in referenceBeatmap.ControlPointInfo.TimingPoints) + newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); + } + else + { + newBeatmap = referenceBeatmap.Clone(); + newBeatmap.BeatmapInfo = newBeatmapInfo; + } beatmapModelManager.Save(newBeatmapInfo, newBeatmap); - workingBeatmapCache.Invalidate(beatmapSetInfo); + workingBeatmapCache.Invalidate(targetBeatmapSet); return GetWorkingBeatmap(newBeatmap.BeatmapInfo); } diff --git a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs new file mode 100644 index 0000000000..472f0e8948 --- /dev/null +++ b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public class CreateNewDifficultyDialog : PopupDialog + { + /// + /// Delegate used to create new difficulties. + /// A value of in the clearAllObjects parameter + /// indicates that the new difficulty should have its hitobjects cleared; + /// otherwise, the new difficulty should be an exact copy of an existing one. + /// + public delegate void CreateNewDifficulty(bool clearAllObjects); + + public CreateNewDifficultyDialog(CreateNewDifficulty createNewDifficulty) + { + HeaderText = "Would you like to clear all objects?"; + + Icon = FontAwesome.Regular.Clone; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = "Yeah, let's start from scratch!", + Action = () => createNewDifficulty.Invoke(true) + }, + new PopupDialogCancelButton + { + Text = "No, create an exact copy of this difficulty", + Action = () => createNewDifficulty.Invoke(false) + }, + new PopupDialogCancelButton + { + Text = "I changed my mind, I want to keep editing this difficulty", + Action = () => { } + } + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2aec63fa65..c5578287e3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -841,7 +841,25 @@ namespace osu.Game.Screens.Edit } protected void CreateNewDifficulty(RulesetInfo rulesetInfo) - => loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo.BeatmapSet, rulesetInfo, GetState()); + { + if (!rulesetInfo.Equals(editorBeatmap.BeatmapInfo.Ruleset)) + { + switchToNewDifficulty(rulesetInfo, true); + return; + } + + dialogOverlay.Push(new CreateNewDifficultyDialog(clearAllObjects => switchToNewDifficulty(rulesetInfo, clearAllObjects))); + } + + private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool clearAllObjects) + => loader?.ScheduleSwitchToNewDifficulty(new NewDifficultyCreationParameters + { + BeatmapSet = editorBeatmap.BeatmapInfo.BeatmapSet, + Ruleset = rulesetInfo, + ReferenceBeatmap = playableBeatmap, + ClearAllObjects = clearAllObjects, + EditorState = GetState() + }); private EditorMenuItem createDifficultySwitchMenu() { diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index de47411fdc..169b601a94 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -11,7 +11,6 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -80,12 +79,12 @@ namespace osu.Game.Screens.Edit } } - public void ScheduleSwitchToNewDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo, EditorState editorState) + public void ScheduleSwitchToNewDifficulty(NewDifficultyCreationParameters creationParameters) => scheduleDifficultySwitch(() => { try { - return beatmapManager.CreateNewBlankDifficulty(beatmapSetInfo, rulesetInfo); + return beatmapManager.CreateNewBlankDifficulty(creationParameters); } catch (Exception ex) { @@ -94,7 +93,7 @@ namespace osu.Game.Screens.Edit Logger.Error(ex, ex.Message); return Beatmap.Value; } - }, editorState); + }, creationParameters.EditorState); public void ScheduleSwitchToExistingDifficulty(BeatmapInfo beatmapInfo, EditorState editorState) => scheduleDifficultySwitch(() => beatmapManager.GetWorkingBeatmap(beatmapInfo), editorState); diff --git a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs new file mode 100644 index 0000000000..dd03fd3644 --- /dev/null +++ b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.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 osu.Game.Beatmaps; +using osu.Game.Rulesets; + +namespace osu.Game.Screens.Edit +{ + public class NewDifficultyCreationParameters + { + /// + /// The that should contain the newly-created difficulty. + /// + public BeatmapSetInfo BeatmapSet { get; set; } + + /// + /// The that the new difficulty should be playable for. + /// + public RulesetInfo Ruleset { get; set; } + + /// + /// A reference upon which the new difficulty should be based. + /// + public IBeatmap ReferenceBeatmap { get; set; } + + /// + /// Whether all objects should be cleared from the new difficulty. + /// + public bool ClearAllObjects { get; set; } + + /// + /// The saved state of the previous which should be restored upon opening the newly-created difficulty. + /// + public EditorState EditorState { get; set; } + } +} diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 331bf04644..ff09598eef 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); } - public override WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + public override WorkingBeatmap CreateNewBlankDifficulty(NewDifficultyCreationParameters creationParameters) { // don't actually care about properly creating a difficulty for this context. return TestBeatmap; From 0d1171b7fae94ea769598bb2b5f0dff194c6c32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 17:56:51 +0100 Subject: [PATCH 509/587] Adjust existing test coverage to pass --- .../Editing/TestSceneEditorBeatmapCreation.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index a14c9aded3..89a9307f9f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -10,6 +10,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; @@ -92,7 +93,7 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestCreateNewDifficulty() + public void TestCreateNewDifficulty([Values] bool sameRuleset) { string firstDifficultyName = Guid.NewGuid().ToString(); string secondDifficultyName = Guid.NewGuid().ToString(); @@ -111,7 +112,14 @@ namespace osu.Game.Tests.Visual.Editing }); AddAssert("can save again", () => Editor.Save()); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(sameRuleset ? new OsuRuleset().RulesetInfo : new CatchRuleset().RulesetInfo)); + + if (sameRuleset) + { + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction()); + } + AddUntilStep("wait for created", () => { string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; @@ -154,7 +162,7 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestCreateNewBeatmapFailsWithSameNamedDifficulties() + public void TestCreateNewBeatmapFailsWithSameNamedDifficulties([Values] bool sameRuleset) { Guid setId = Guid.Empty; const string duplicate_difficulty_name = "duplicate"; @@ -168,7 +176,14 @@ namespace osu.Game.Tests.Visual.Editing return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); }); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(sameRuleset ? new OsuRuleset().RulesetInfo : new CatchRuleset().RulesetInfo)); + + if (sameRuleset) + { + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction()); + } + AddUntilStep("wait for created", () => { string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; From eb939547a9978e0d4e47a65f90c3b5b902b7d988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 18:03:54 +0100 Subject: [PATCH 510/587] Add test coverage for difficulty copy flow --- .../Editing/TestSceneEditorBeatmapCreation.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 89a9307f9f..91f667d8b0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -9,13 +9,17 @@ using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; using osu.Game.Storyboards; using osu.Game.Tests.Resources; +using osuTK; using SharpCompress.Archives; using SharpCompress.Archives.Zip; @@ -99,6 +103,21 @@ namespace osu.Game.Tests.Visual.Editing string secondDifficultyName = Guid.NewGuid().ToString(); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] + { + new HitCircle + { + Position = new Vector2(0), + StartTime = 0 + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE, + StartTime = 1000 + } + })); + AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => { @@ -126,6 +145,80 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != firstDifficultyName; }); + AddAssert("created difficulty has timing point", () => + { + var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); + return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; + }); + AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName); + var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + + return beatmap != null + && beatmap.DifficultyName == secondDifficultyName + && set != null + && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); + }); + } + + [Test] + public void TestCopyDifficulty() + { + string firstDifficultyName = Guid.NewGuid().ToString(); + string secondDifficultyName = Guid.NewGuid().ToString(); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] + { + new HitCircle + { + Position = new Vector2(0), + StartTime = 0 + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE, + StartTime = 1000 + } + })); + + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName); + var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + + return beatmap != null + && beatmap.DifficultyName == firstDifficultyName + && set != null + && set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID); + }); + AddAssert("can save again", () => Editor.Save()); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick()); + + AddUntilStep("wait for created", () => + { + string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != firstDifficultyName; + }); + + AddAssert("created difficulty has timing point", () => + { + var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); + return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; + }); + AddAssert("created difficulty has objects", () => EditorBeatmap.HitObjects.Count == 2); + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => From fd1c8c361444f1bd7077379d64a1c03e5576c0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 18:26:19 +0100 Subject: [PATCH 511/587] Add failing test coverage for correct beatmap difficulty copy --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 91f667d8b0..672e643e60 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -187,6 +187,7 @@ namespace osu.Game.Tests.Visual.Editing StartTime = 1000 } })); + AddStep("set approach rate", () => EditorBeatmap.Difficulty.ApproachRate = 4); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => @@ -218,6 +219,7 @@ namespace osu.Game.Tests.Visual.Editing return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; }); AddAssert("created difficulty has objects", () => EditorBeatmap.HitObjects.Count == 2); + AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); From 1bf5375e746f6be78a992594fda76b821b72cb88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 18:40:51 +0100 Subject: [PATCH 512/587] Fix `BeatmapInfo`-associated member not copying --- osu.Game/Beatmaps/BeatmapManager.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index bd9cdba9fb..c350cfc111 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -118,18 +118,12 @@ namespace osu.Game.Beatmaps var referenceBeatmap = creationParameters.ReferenceBeatmap; var targetBeatmapSet = creationParameters.BeatmapSet; - var newBeatmapInfo = new BeatmapInfo(creationParameters.Ruleset, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); - - // populate circular beatmap set info <-> beatmap info references manually. - // several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()` - // rely on them being freely traversable in both directions for correct operation. - targetBeatmapSet.Beatmaps.Add(newBeatmapInfo); - newBeatmapInfo.BeatmapSet = targetBeatmapSet; - + BeatmapInfo newBeatmapInfo; IBeatmap newBeatmap; if (creationParameters.ClearAllObjects) { + newBeatmapInfo = new BeatmapInfo(creationParameters.Ruleset, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; foreach (var timingPoint in referenceBeatmap.ControlPointInfo.TimingPoints) newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); @@ -137,9 +131,19 @@ namespace osu.Game.Beatmaps else { newBeatmap = referenceBeatmap.Clone(); - newBeatmap.BeatmapInfo = newBeatmapInfo; + newBeatmap.BeatmapInfo = newBeatmapInfo = referenceBeatmap.BeatmapInfo.Clone(); + // assign a new ID to the clone. + newBeatmapInfo.ID = Guid.NewGuid(); + // clear difficulty name to avoid clashes on save. + newBeatmapInfo.DifficultyName = string.Empty; } + // populate circular beatmap set info <-> beatmap info references manually. + // several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()` + // rely on them being freely traversable in both directions for correct operation. + targetBeatmapSet.Beatmaps.Add(newBeatmapInfo); + newBeatmapInfo.BeatmapSet = targetBeatmapSet; + beatmapModelManager.Save(newBeatmapInfo, newBeatmap); workingBeatmapCache.Invalidate(targetBeatmapSet); From 1292722a00e331933852f1842455a3e25108c4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 18:44:12 +0100 Subject: [PATCH 513/587] Add failing test coverage for correct combo colour copy --- .../Editing/TestSceneEditorBeatmapCreation.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 672e643e60..90b1d3a6f9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -6,6 +6,8 @@ using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -188,6 +190,16 @@ namespace osu.Game.Tests.Visual.Editing } })); AddStep("set approach rate", () => EditorBeatmap.Difficulty.ApproachRate = 4); + AddStep("set combo colours", () => + { + var beatmapSkin = EditorBeatmap.BeatmapSkin.AsNonNull(); + beatmapSkin.ComboColours.Clear(); + beatmapSkin.ComboColours.AddRange(new[] + { + new Colour4(255, 0, 0, 255), + new Colour4(0, 0, 255, 255) + }); + }); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => @@ -220,6 +232,7 @@ namespace osu.Game.Tests.Visual.Editing }); AddAssert("created difficulty has objects", () => EditorBeatmap.HitObjects.Count == 2); AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); + AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); From a144d6f8d67b0fe6c90021b3309682f323da9322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 18:50:11 +0100 Subject: [PATCH 514/587] Fix beatmap skin properties not copying --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 16 +++++---- .../Edit/NewDifficultyCreationParameters.cs | 34 ++++++++++++++++--- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c350cfc111..a9d0576696 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -144,7 +144,7 @@ namespace osu.Game.Beatmaps targetBeatmapSet.Beatmaps.Add(newBeatmapInfo); newBeatmapInfo.BeatmapSet = targetBeatmapSet; - beatmapModelManager.Save(newBeatmapInfo, newBeatmap); + beatmapModelManager.Save(newBeatmapInfo, newBeatmap, creationParameters.ReferenceBeatmapSkin); workingBeatmapCache.Invalidate(targetBeatmapSet); return GetWorkingBeatmap(newBeatmap.BeatmapInfo); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c5578287e3..37d4dce60e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -9,6 +9,7 @@ using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -853,13 +854,14 @@ namespace osu.Game.Screens.Edit private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool clearAllObjects) => loader?.ScheduleSwitchToNewDifficulty(new NewDifficultyCreationParameters - { - BeatmapSet = editorBeatmap.BeatmapInfo.BeatmapSet, - Ruleset = rulesetInfo, - ReferenceBeatmap = playableBeatmap, - ClearAllObjects = clearAllObjects, - EditorState = GetState() - }); + ( + editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull(), + rulesetInfo, + playableBeatmap, + editorBeatmap.BeatmapSkin, + clearAllObjects, + GetState() + )); private EditorMenuItem createDifficultySwitchMenu() { diff --git a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs index dd03fd3644..aa7dac609b 100644 --- a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs +++ b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Skinning; namespace osu.Game.Screens.Edit { @@ -11,26 +14,47 @@ namespace osu.Game.Screens.Edit /// /// The that should contain the newly-created difficulty. /// - public BeatmapSetInfo BeatmapSet { get; set; } + public BeatmapSetInfo BeatmapSet { get; } /// /// The that the new difficulty should be playable for. /// - public RulesetInfo Ruleset { get; set; } + public RulesetInfo Ruleset { get; } /// /// A reference upon which the new difficulty should be based. /// - public IBeatmap ReferenceBeatmap { get; set; } + public IBeatmap ReferenceBeatmap { get; } + + /// + /// A reference that the new difficulty should base its own skin upon. + /// + public ISkin? ReferenceBeatmapSkin { get; } /// /// Whether all objects should be cleared from the new difficulty. /// - public bool ClearAllObjects { get; set; } + public bool ClearAllObjects { get; } /// /// The saved state of the previous which should be restored upon opening the newly-created difficulty. /// - public EditorState EditorState { get; set; } + public EditorState EditorState { get; } + + public NewDifficultyCreationParameters( + BeatmapSetInfo beatmapSet, + RulesetInfo ruleset, + IBeatmap referenceBeatmap, + ISkin? referenceBeatmapSkin, + bool clearAllObjects, + EditorState editorState) + { + BeatmapSet = beatmapSet; + Ruleset = ruleset; + ReferenceBeatmap = referenceBeatmap; + ReferenceBeatmapSkin = referenceBeatmapSkin; + ClearAllObjects = clearAllObjects; + EditorState = editorState; + } } } From 6fd663a718b30de3b3064edc4a986d4cc61dfa2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 19:51:02 +0100 Subject: [PATCH 515/587] Apply some renames to convey difference between creation options better --- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++-- osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs | 2 +- osu.Game/Screens/Edit/EditorLoader.cs | 2 +- .../Screens/Edit/NewDifficultyCreationParameters.cs | 13 +++++++++---- osu.Game/Tests/Visual/EditorTestScene.cs | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a9d0576696..87051ef650 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -113,7 +113,7 @@ namespace osu.Game.Beatmaps /// The new difficulty will be backed by a model /// and represented by the returned . /// - public virtual WorkingBeatmap CreateNewBlankDifficulty(NewDifficultyCreationParameters creationParameters) + public virtual WorkingBeatmap CreateNewDifficulty(NewDifficultyCreationParameters creationParameters) { var referenceBeatmap = creationParameters.ReferenceBeatmap; var targetBeatmapSet = creationParameters.BeatmapSet; @@ -121,7 +121,7 @@ namespace osu.Game.Beatmaps BeatmapInfo newBeatmapInfo; IBeatmap newBeatmap; - if (creationParameters.ClearAllObjects) + if (creationParameters.CreateBlank) { newBeatmapInfo = new BeatmapInfo(creationParameters.Ruleset, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; diff --git a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs index 472f0e8948..138e13bda1 100644 --- a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs +++ b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Edit public CreateNewDifficultyDialog(CreateNewDifficulty createNewDifficulty) { - HeaderText = "Would you like to clear all objects?"; + HeaderText = "Would you like to create a blank difficulty?"; Icon = FontAwesome.Regular.Clone; diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 169b601a94..be3e68c857 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.Edit { try { - return beatmapManager.CreateNewBlankDifficulty(creationParameters); + return beatmapManager.CreateNewDifficulty(creationParameters); } catch (Exception ex) { diff --git a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs index aa7dac609b..a6458a9456 100644 --- a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs +++ b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs @@ -32,9 +32,14 @@ namespace osu.Game.Screens.Edit public ISkin? ReferenceBeatmapSkin { get; } /// - /// Whether all objects should be cleared from the new difficulty. + /// Whether the new difficulty should be blank. /// - public bool ClearAllObjects { get; } + /// + /// A blank difficulty will have no objects, no control points other than timing points taken from + /// and will not share values with , + /// but it will share metadata and timing information with . + /// + public bool CreateBlank { get; } /// /// The saved state of the previous which should be restored upon opening the newly-created difficulty. @@ -46,14 +51,14 @@ namespace osu.Game.Screens.Edit RulesetInfo ruleset, IBeatmap referenceBeatmap, ISkin? referenceBeatmapSkin, - bool clearAllObjects, + bool createBlank, EditorState editorState) { BeatmapSet = beatmapSet; Ruleset = ruleset; ReferenceBeatmap = referenceBeatmap; ReferenceBeatmapSkin = referenceBeatmapSkin; - ClearAllObjects = clearAllObjects; + CreateBlank = createBlank; EditorState = editorState; } } diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index ff09598eef..8c8a106791 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); } - public override WorkingBeatmap CreateNewBlankDifficulty(NewDifficultyCreationParameters creationParameters) + public override WorkingBeatmap CreateNewDifficulty(NewDifficultyCreationParameters creationParameters) { // don't actually care about properly creating a difficulty for this context. return TestBeatmap; From 90c48de9f8d68d2cdafcdde863ea671320449e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 20:01:47 +0100 Subject: [PATCH 516/587] Add failing test coverage for save of copied beatmap keeping old beatmap file --- .../Editing/TestSceneEditorBeatmapCreation.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 90b1d3a6f9..0025e88e62 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -12,6 +12,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; @@ -236,16 +237,24 @@ namespace osu.Game.Tests.Visual.Editing AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); + + BeatmapInfo refetchedBeatmap = null; + Live refetchedBeatmapSet = null; + + AddStep("refetch from database", () => + { + refetchedBeatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName); + refetchedBeatmapSet = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + }); + AddAssert("new beatmap persisted", () => { - var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName); - var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); - - return beatmap != null - && beatmap.DifficultyName == secondDifficultyName - && set != null - && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); + return refetchedBeatmap != null + && refetchedBeatmap.DifficultyName == secondDifficultyName + && refetchedBeatmapSet != null + && refetchedBeatmapSet.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); }); + AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2)); } [Test] From ecd6a68c6f0814cbd9a646f4dca4abf758162276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 20:05:32 +0100 Subject: [PATCH 517/587] Clear hash when creating copy of existing difficulty --- osu.Game/Beatmaps/BeatmapManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 87051ef650..4fa07ff518 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -136,6 +136,8 @@ namespace osu.Game.Beatmaps newBeatmapInfo.ID = Guid.NewGuid(); // clear difficulty name to avoid clashes on save. newBeatmapInfo.DifficultyName = string.Empty; + // also clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. + newBeatmapInfo.Hash = string.Empty; } // populate circular beatmap set info <-> beatmap info references manually. From 13abc392bd139305ac876df335139580ce2f4035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Feb 2022 18:54:52 +0100 Subject: [PATCH 518/587] Add failing test coverage for not copying online properties --- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 0025e88e62..b4559ac9fa 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -201,6 +201,11 @@ namespace osu.Game.Tests.Visual.Editing new Colour4(0, 0, 255, 255) }); }); + AddStep("set status & online ID", () => + { + EditorBeatmap.BeatmapInfo.OnlineID = 123456; + EditorBeatmap.BeatmapInfo.Status = BeatmapOnlineStatus.WIP; + }); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => @@ -235,6 +240,9 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2); + AddAssert("status not copied", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None); + AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1); + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); From 5dabc9282c0e1e3be5538cd4c7996d67eae6197e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Feb 2022 19:04:11 +0100 Subject: [PATCH 519/587] Change `BeatmapInfo` copy logic to be opt-in rather than opt-out --- osu.Game/Beatmaps/BeatmapManager.cs | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4fa07ff518..4dd0e08dab 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -131,13 +131,28 @@ namespace osu.Game.Beatmaps else { newBeatmap = referenceBeatmap.Clone(); - newBeatmap.BeatmapInfo = newBeatmapInfo = referenceBeatmap.BeatmapInfo.Clone(); - // assign a new ID to the clone. - newBeatmapInfo.ID = Guid.NewGuid(); - // clear difficulty name to avoid clashes on save. - newBeatmapInfo.DifficultyName = string.Empty; - // also clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. - newBeatmapInfo.Hash = string.Empty; + + var referenceBeatmapInfo = referenceBeatmap.BeatmapInfo; + newBeatmap.BeatmapInfo = newBeatmapInfo = new BeatmapInfo(referenceBeatmapInfo.Ruleset, referenceBeatmapInfo.Difficulty.Clone(), referenceBeatmapInfo.Metadata.DeepClone()) + { + // Only selected appropriate properties are copied over. + // Things like database ID, online status/ID, MD5 hash, star rating, etc. are omitted + // because they should not be copied over and/or they will be recomputed on save. + AudioLeadIn = referenceBeatmapInfo.AudioLeadIn, + StackLeniency = referenceBeatmapInfo.StackLeniency, + SpecialStyle = referenceBeatmapInfo.SpecialStyle, + LetterboxInBreaks = referenceBeatmapInfo.LetterboxInBreaks, + WidescreenStoryboard = referenceBeatmapInfo.WidescreenStoryboard, + EpilepsyWarning = referenceBeatmapInfo.EpilepsyWarning, + SamplesMatchPlaybackRate = referenceBeatmapInfo.SamplesMatchPlaybackRate, + DistanceSpacing = referenceBeatmapInfo.DistanceSpacing, + BeatDivisor = referenceBeatmapInfo.BeatDivisor, + GridSize = referenceBeatmapInfo.GridSize, + TimelineZoom = referenceBeatmapInfo.TimelineZoom, + Countdown = referenceBeatmapInfo.Countdown, + CountdownOffset = referenceBeatmapInfo.CountdownOffset, + Bookmarks = (int[])referenceBeatmapInfo.Bookmarks.Clone() + }; } // populate circular beatmap set info <-> beatmap info references manually. From b9d9fc56afa5f579389201f8d06bfa41b3c3980b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 17:51:39 +0900 Subject: [PATCH 520/587] Move files to UI namespace --- .../Visual/UserInterface/TestSceneExpandingContainer.cs | 2 ++ .../Containers}/ExpandingButtonContainer.cs | 2 +- .../{Overlays => Graphics/Containers}/ExpandingContainer.cs | 3 +-- osu.Game/{Overlays => Graphics/Containers}/IExpandable.cs | 2 +- .../{Overlays => Graphics/Containers}/IExpandingContainer.cs | 3 ++- .../{Overlays => Graphics/UserInterface}/ExpandableSlider.cs | 4 ++-- osu.Game/Overlays/Settings/SettingsSidebar.cs | 1 + osu.Game/Overlays/SettingsToolboxGroup.cs | 1 + osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 9 files changed, 12 insertions(+), 8 deletions(-) rename osu.Game/{Overlays => Graphics/Containers}/ExpandingButtonContainer.cs (94%) rename osu.Game/{Overlays => Graphics/Containers}/ExpandingContainer.cs (97%) rename osu.Game/{Overlays => Graphics/Containers}/IExpandable.cs (93%) rename osu.Game/{Overlays => Graphics/Containers}/IExpandingContainer.cs (88%) rename osu.Game/{Overlays => Graphics/UserInterface}/ExpandableSlider.cs (97%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs index f63591311f..f4920b4412 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs @@ -4,6 +4,8 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections; using osuTK; diff --git a/osu.Game/Overlays/ExpandingButtonContainer.cs b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs similarity index 94% rename from osu.Game/Overlays/ExpandingButtonContainer.cs rename to osu.Game/Graphics/Containers/ExpandingButtonContainer.cs index 8fb3e1b550..b79af22bd2 100644 --- a/osu.Game/Overlays/ExpandingButtonContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -namespace osu.Game.Overlays +namespace osu.Game.Graphics.Containers { /// /// An with a long hover expansion delay. diff --git a/osu.Game/Overlays/ExpandingContainer.cs b/osu.Game/Graphics/Containers/ExpandingContainer.cs similarity index 97% rename from osu.Game/Overlays/ExpandingContainer.cs rename to osu.Game/Graphics/Containers/ExpandingContainer.cs index ea3fffcb78..b50e008362 100644 --- a/osu.Game/Overlays/ExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingContainer.cs @@ -6,9 +6,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Threading; -using osu.Game.Graphics.Containers; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.Containers { /// /// Represents a with the ability to expand/contract on hover. diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Graphics/Containers/IExpandable.cs similarity index 93% rename from osu.Game/Overlays/IExpandable.cs rename to osu.Game/Graphics/Containers/IExpandable.cs index 770ac97847..593564a2f9 100644 --- a/osu.Game/Overlays/IExpandable.cs +++ b/osu.Game/Graphics/Containers/IExpandable.cs @@ -4,7 +4,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.Containers { /// /// An interface for drawables with ability to expand/contract. diff --git a/osu.Game/Overlays/IExpandingContainer.cs b/osu.Game/Graphics/Containers/IExpandingContainer.cs similarity index 88% rename from osu.Game/Overlays/IExpandingContainer.cs rename to osu.Game/Graphics/Containers/IExpandingContainer.cs index ec5f0c90f4..a82faa3cd1 100644 --- a/osu.Game/Overlays/IExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/IExpandingContainer.cs @@ -3,8 +3,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.Containers { /// /// A target expanding container that should be resolved by children s to propagate state changes. diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs similarity index 97% rename from osu.Game/Overlays/ExpandableSlider.cs rename to osu.Game/Graphics/UserInterface/ExpandableSlider.cs index 062de98659..60e83f9c81 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs @@ -8,11 +8,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osuTK; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.UserInterface { /// /// An implementation for the UI slider bar control. diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index e6ce90c33e..4e6a1eb914 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Settings { diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index 9e7223df9d..08321f68fe 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osuTK; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 92ea2db338..39783cc8bb 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -13,7 +13,7 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Overlays; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; From 3aa5908de8d0bd691c5a5839130e3ce66fabe107 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 18:01:56 +0900 Subject: [PATCH 521/587] Remove unused using statement --- osu.Game/Graphics/Containers/IExpandingContainer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/IExpandingContainer.cs b/osu.Game/Graphics/Containers/IExpandingContainer.cs index a82faa3cd1..eb186c96a8 100644 --- a/osu.Game/Graphics/Containers/IExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/IExpandingContainer.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; -using osu.Game.Overlays; namespace osu.Game.Graphics.Containers { From e324287f796128a722a4d94435b02b90f356076e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 18:08:16 +0900 Subject: [PATCH 522/587] Reduce expansion delay on `ExpandingButtonContainer` Felt too long. --- osu.Game/Graphics/Containers/ExpandingButtonContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs index b79af22bd2..859850e771 100644 --- a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs @@ -16,6 +16,6 @@ namespace osu.Game.Graphics.Containers { } - protected override double HoverExpansionDelay => 750; + protected override double HoverExpansionDelay => 400; } } From e304c031dcdffdcbd66f0f896ee160f79ee7a86d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 21:53:56 +0900 Subject: [PATCH 523/587] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1a2859c851..6b3142fbdc 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index a9c0226951..847832f8ac 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 4a1dc53281..00c9653146 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From c5019fefb0c9ed0a212167851acc082fe5f4e618 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 22:35:08 +0900 Subject: [PATCH 524/587] Update CI runs to target net6.0 --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c52802cf6..ec3816d541 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Install .NET 5.0.x + - name: Install .NET 6.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: "5.0.x" + dotnet-version: "6.0.x" # FIXME: libavformat is not included in Ubuntu. Let's fix that. # https://github.com/ppy/osu-framework/issues/4349 @@ -65,10 +65,10 @@ jobs: run: | $VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2 - - name: Install .NET 5.0.x + - name: Install .NET 6.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: "5.0.x" + dotnet-version: "6.0.x" # Contrary to seemingly any other msbuild, msbuild running on macOS/Mono # cannot accept .sln(f) files as arguments. @@ -84,10 +84,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Install .NET 5.0.x + - name: Install .NET 6.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: "5.0.x" + dotnet-version: "6.0.x" # Contrary to seemingly any other msbuild, msbuild running on macOS/Mono # cannot accept .sln(f) files as arguments. @@ -102,17 +102,17 @@ jobs: - name: Checkout uses: actions/checkout@v2 - # FIXME: Tools won't run in .NET 5.0 unless you install 3.1.x LTS side by side. + # FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side. # https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e - name: Install .NET 3.1.x LTS uses: actions/setup-dotnet@v1 with: dotnet-version: "3.1.x" - - name: Install .NET 5.0.x + - name: Install .NET 6.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: "5.0.x" + dotnet-version: "6.0.x" - name: Restore Tools run: dotnet tool restore From 70ba6fb7dc2e48712886a6ae3ad4eb22079e4136 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 22:35:30 +0900 Subject: [PATCH 525/587] Update .NET version in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1dfcab416..7ace47a74f 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ You can see some examples of custom rulesets by visiting the [custom ruleset dir Please make sure you have the following prerequisites: -- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) installed. +- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed. - When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). - When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). - When running on Linux, please have a system-wide FFmpeg installation available to support video decoding. From 9b7d9d42bcd01f84375cc38d5f6bf4ab74f996a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 22:48:04 +0900 Subject: [PATCH 526/587] Update reference to `NetAnalyzers` --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 894ea25c8b..6e7015fa26 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,7 @@ - + $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset From 9ad7b5d51caab9433c2eaaecbce6a1e4dc687516 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 22:52:30 +0900 Subject: [PATCH 527/587] Remove no longer required `NoWarn` spec --- Directory.Build.props | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6e7015fa26..c1682638c2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -32,13 +32,8 @@ NU1701: DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway. This is required due to https://github.com/NuGet/Home/issues/5740 - - CA9998: - Microsoft.CodeAnalysis.FxCopAnalyzers has been deprecated. - The entire package will be able to be removed after migrating to .NET 5, - as analysers are shipped as part of the .NET 5 SDK anyway. --> - $(NoWarn);NU1701;CA9998 + $(NoWarn);NU1701 false From b581ca14cca29e7c8fb28bcfbb55e0aec0ae7389 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 02:32:38 +0900 Subject: [PATCH 528/587] Update usages in line with `BorderColour` type change --- osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs index deb2e6baf6..c6477d1781 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs @@ -114,7 +114,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 private class CircularBorderContainer : CircularContainer { - public void TransformBorderTo(SRGBColour colour) + public void TransformBorderTo(ColourInfo colour) => this.TransformTo(nameof(BorderColour), colour, 250, Easing.OutQuint); } } From 02f58a82fc4682df861a6069a12316d3bf474084 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 02:35:08 +0900 Subject: [PATCH 529/587] Use `WaitSafely()` in tests where it was not already being used --- .../Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs | 3 ++- .../Multiplayer/TestSceneMultiplayerPlaylist.cs | 10 +++++----- .../Multiplayer/TestSceneMultiplayerQueueList.cs | 12 ++++++------ .../Visual/Multiplayer/TestSceneTeamVersus.cs | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index c7eeff81fe..cd14a98751 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; @@ -71,7 +72,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers - })); + }).WaitSafely()); AddUntilStep("api room updated", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 936798e6b4..5dd9fb9fe2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Multiplayer importedBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); }); - AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); } [Test] @@ -97,19 +97,19 @@ namespace osu.Game.Tests.Visual.Multiplayer addItemStep(); addItemStep(); - AddStep("finish current item", () => Client.FinishCurrentItem()); + AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(1, 0); assertItemInQueueListStep(2, 0); assertItemInQueueListStep(3, 1); - AddStep("finish current item", () => Client.FinishCurrentItem()); + AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(2, 0); assertItemInHistoryListStep(1, 1); assertItemInQueueListStep(3, 0); - AddStep("finish current item", () => Client.FinishCurrentItem()); + AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(3, 0); assertItemInHistoryListStep(2, 1); @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestListsClearedWhenRoomLeft() { addItemStep(); - AddStep("finish current item", () => Client.FinishCurrentItem()); + AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); AddStep("leave room", () => RoomManager.PartRoom()); AddUntilStep("wait for room part", () => !RoomJoined); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index ddf794b437..80b20ed59f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -65,13 +65,13 @@ namespace osu.Game.Tests.Visual.Multiplayer importedBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); }); - AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); } [Test] public void TestDeleteButtonAlwaysVisibleForHost() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); addPlaylistItem(() => API.LocalUser.Value.OnlineID); @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); AddStep("join other user", () => Client.AddUser(new APIUser { Id = 1234 })); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestCurrentItemDoesNotHaveDeleteButton() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); addPlaylistItem(() => API.LocalUser.Value.OnlineID); @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Multiplayer assertDeleteButtonVisibility(0, false); assertDeleteButtonVisibility(1, true); - AddStep("finish current item", () => Client.FinishCurrentItem()); + AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); AddUntilStep("wait for next item to be selected", () => Client.Room?.Settings.PlaylistItemId == 2); AddUntilStep("wait for two items in playlist", () => playlist.ChildrenOfType().Count() == 2); @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapID = importedBeatmap.OnlineID, }); - Client.AddUserPlaylistItem(userId(), item); + Client.AddUserPlaylistItem(userId(), item).WaitSafely(); itemId = item.ID; }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 781f0a1824..2837d75553 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change match type", () => client.ChangeSettings(new MultiplayerRoomSettings { MatchType = MatchType.TeamVersus - })); + }).WaitSafely()); AddUntilStep("api room updated to team versus", () => client.APIRoom?.Type.Value == MatchType.TeamVersus); } From 8da0800d7fc70d213ab5401e97d537f308d75cf5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 02:47:35 +0900 Subject: [PATCH 530/587] Update `ChangeFocus` usage in line with framework changes --- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 3fd56ece58..27743e709f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -246,7 +246,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { base.LoadComplete(); - Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextBox)); + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(passwordTextBox)); passwordTextBox.OnCommit += (_, __) => performJoin(); } From 4bd58cfde160398726aaffb80e93d630a34029f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 18:52:19 +0100 Subject: [PATCH 531/587] Update one more custom transform with `BorderColour` type change --- osu.Game/Overlays/Volume/MuteButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Volume/MuteButton.cs b/osu.Game/Overlays/Volume/MuteButton.cs index bcc9394aba..e9d3b31207 100644 --- a/osu.Game/Overlays/Volume/MuteButton.cs +++ b/osu.Game/Overlays/Volume/MuteButton.cs @@ -79,13 +79,13 @@ namespace osu.Game.Overlays.Volume protected override bool OnHover(HoverEvent e) { - Content.TransformTo, SRGBColour>("BorderColour", hoveredColour, 500, Easing.OutQuint); + Content.TransformTo, ColourInfo>("BorderColour", hoveredColour, 500, Easing.OutQuint); return false; } protected override void OnHoverLost(HoverLostEvent e) { - Content.TransformTo, SRGBColour>("BorderColour", unhoveredColour, 500, Easing.OutQuint); + Content.TransformTo, ColourInfo>("BorderColour", unhoveredColour, 500, Easing.OutQuint); } } } From db74a226c0b3e5d59493075b124b4dc1e8fdb0a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 02:54:45 +0900 Subject: [PATCH 532/587] Fix test regression due to mouse overlapping settings overlay --- osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs index e34ec6c46a..bbab6380ba 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs @@ -19,6 +19,10 @@ namespace osu.Game.Tests.Visual.Menus base.SetUpSteps(); AddAssert("no screen offset applied", () => Game.ScreenOffsetContainer.X == 0f); + + // avoids mouse interacting with settings overlay. + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for overlays", () => Game.Settings.IsLoaded && Game.Notifications.IsLoaded); } From 7e75fa7117ebabf0e5cb1c8ad1b5e5d79349047a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 19:52:08 +0100 Subject: [PATCH 533/587] Revert "Change `BeatmapInfo` copy logic to be opt-in rather than opt-out" This reverts commit 5dabc9282c0e1e3be5538cd4c7996d67eae6197e. --- osu.Game/Beatmaps/BeatmapManager.cs | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4dd0e08dab..4fa07ff518 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -131,28 +131,13 @@ namespace osu.Game.Beatmaps else { newBeatmap = referenceBeatmap.Clone(); - - var referenceBeatmapInfo = referenceBeatmap.BeatmapInfo; - newBeatmap.BeatmapInfo = newBeatmapInfo = new BeatmapInfo(referenceBeatmapInfo.Ruleset, referenceBeatmapInfo.Difficulty.Clone(), referenceBeatmapInfo.Metadata.DeepClone()) - { - // Only selected appropriate properties are copied over. - // Things like database ID, online status/ID, MD5 hash, star rating, etc. are omitted - // because they should not be copied over and/or they will be recomputed on save. - AudioLeadIn = referenceBeatmapInfo.AudioLeadIn, - StackLeniency = referenceBeatmapInfo.StackLeniency, - SpecialStyle = referenceBeatmapInfo.SpecialStyle, - LetterboxInBreaks = referenceBeatmapInfo.LetterboxInBreaks, - WidescreenStoryboard = referenceBeatmapInfo.WidescreenStoryboard, - EpilepsyWarning = referenceBeatmapInfo.EpilepsyWarning, - SamplesMatchPlaybackRate = referenceBeatmapInfo.SamplesMatchPlaybackRate, - DistanceSpacing = referenceBeatmapInfo.DistanceSpacing, - BeatDivisor = referenceBeatmapInfo.BeatDivisor, - GridSize = referenceBeatmapInfo.GridSize, - TimelineZoom = referenceBeatmapInfo.TimelineZoom, - Countdown = referenceBeatmapInfo.Countdown, - CountdownOffset = referenceBeatmapInfo.CountdownOffset, - Bookmarks = (int[])referenceBeatmapInfo.Bookmarks.Clone() - }; + newBeatmap.BeatmapInfo = newBeatmapInfo = referenceBeatmap.BeatmapInfo.Clone(); + // assign a new ID to the clone. + newBeatmapInfo.ID = Guid.NewGuid(); + // clear difficulty name to avoid clashes on save. + newBeatmapInfo.DifficultyName = string.Empty; + // also clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. + newBeatmapInfo.Hash = string.Empty; } // populate circular beatmap set info <-> beatmap info references manually. From 40cfee34211387866835612abf142b893b5498c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 19:54:40 +0100 Subject: [PATCH 534/587] Explicitly reset online ID and beatmap status on copy --- osu.Game/Beatmaps/BeatmapManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4fa07ff518..884209ee56 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -136,8 +136,11 @@ namespace osu.Game.Beatmaps newBeatmapInfo.ID = Guid.NewGuid(); // clear difficulty name to avoid clashes on save. newBeatmapInfo.DifficultyName = string.Empty; - // also clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. + // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. newBeatmapInfo.Hash = string.Empty; + // clear online properties. + newBeatmapInfo.OnlineID = -1; + newBeatmapInfo.Status = BeatmapOnlineStatus.None; } // populate circular beatmap set info <-> beatmap info references manually. From 1685e214d36463e225971c6ffef3fad73aae0735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 19:59:54 +0100 Subject: [PATCH 535/587] Adjust test coverage to cover desired copy naming scheme --- .../Editing/TestSceneEditorBeatmapCreation.cs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index b4559ac9fa..0a2f622da1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -172,10 +172,10 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestCopyDifficulty() { - string firstDifficultyName = Guid.NewGuid().ToString(); - string secondDifficultyName = Guid.NewGuid().ToString(); + string originalDifficultyName = Guid.NewGuid().ToString(); + string copyDifficultyName = $"{originalDifficultyName} (copy)"; - AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = originalDifficultyName); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { @@ -210,11 +210,11 @@ namespace osu.Game.Tests.Visual.Editing AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => { - var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName); + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == originalDifficultyName); var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); return beatmap != null - && beatmap.DifficultyName == firstDifficultyName + && beatmap.DifficultyName == originalDifficultyName && set != null && set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID); }); @@ -228,9 +228,10 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("wait for created", () => { string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; - return difficultyName != null && difficultyName != firstDifficultyName; + return difficultyName != null && difficultyName != originalDifficultyName; }); + AddAssert("created difficulty has copy suffix in name", () => EditorBeatmap.BeatmapInfo.DifficultyName == copyDifficultyName); AddAssert("created difficulty has timing point", () => { var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); @@ -243,7 +244,6 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("status not copied", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None); AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1); - AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); BeatmapInfo refetchedBeatmap = null; @@ -251,16 +251,19 @@ namespace osu.Game.Tests.Visual.Editing AddStep("refetch from database", () => { - refetchedBeatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName); + refetchedBeatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == copyDifficultyName); refetchedBeatmapSet = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); }); AddAssert("new beatmap persisted", () => { return refetchedBeatmap != null - && refetchedBeatmap.DifficultyName == secondDifficultyName + && refetchedBeatmap.DifficultyName == copyDifficultyName && refetchedBeatmapSet != null - && refetchedBeatmapSet.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); + && refetchedBeatmapSet.PerformRead(s => + s.Beatmaps.Count == 2 + && s.Beatmaps.Any(b => b.DifficultyName == originalDifficultyName) + && s.Beatmaps.Any(b => b.DifficultyName == copyDifficultyName)); }); AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2)); } From 62214471647382ff03bf1449db2c03bde8cfa9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 20:19:12 +0100 Subject: [PATCH 536/587] Append copy suffix on creating copy of difficulty --- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++-- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Edit/EditorLoader.cs | 9 ++++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 884209ee56..d1b8e88743 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -134,8 +134,8 @@ namespace osu.Game.Beatmaps newBeatmap.BeatmapInfo = newBeatmapInfo = referenceBeatmap.BeatmapInfo.Clone(); // assign a new ID to the clone. newBeatmapInfo.ID = Guid.NewGuid(); - // clear difficulty name to avoid clashes on save. - newBeatmapInfo.DifficultyName = string.Empty; + // add "(copy)" suffix to difficulty name to avoid clashes on save. + newBeatmapInfo.DifficultyName += " (copy)"; // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. newBeatmapInfo.Hash = string.Empty; // clear online properties. diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 37d4dce60e..7a3c4f2a19 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -857,7 +857,7 @@ namespace osu.Game.Screens.Edit ( editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull(), rulesetInfo, - playableBeatmap, + editorBeatmap, editorBeatmap.BeatmapSkin, clearAllObjects, GetState() diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index be3e68c857..505a57f157 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -84,7 +84,14 @@ namespace osu.Game.Screens.Edit { try { - return beatmapManager.CreateNewDifficulty(creationParameters); + var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(creationParameters.ReferenceBeatmap.BeatmapInfo); + return beatmapManager.CreateNewDifficulty(new NewDifficultyCreationParameters( + refetchedBeatmap.BeatmapSetInfo, + refetchedBeatmap.BeatmapInfo.Ruleset, + refetchedBeatmap.Beatmap, + refetchedBeatmap.Skin, + creationParameters.CreateBlank, + creationParameters.EditorState)); } catch (Exception ex) { From e45a2ae0fc9b24af51454589be31923cb80543ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 20:56:05 +0100 Subject: [PATCH 537/587] Restructure difficulty copy flow to adapt to latest changes --- osu.Game/Beatmaps/BeatmapManager.cs | 83 +++++++++++-------- .../Screens/Edit/CreateNewDifficultyDialog.cs | 12 +-- osu.Game/Screens/Edit/Editor.cs | 27 ++---- osu.Game/Screens/Edit/EditorLoader.cs | 21 ++--- .../Edit/NewDifficultyCreationParameters.cs | 65 --------------- osu.Game/Tests/Visual/EditorTestScene.cs | 8 +- 6 files changed, 83 insertions(+), 133 deletions(-) delete mode 100644 osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d1b8e88743..777d5db2ad 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -20,7 +20,6 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; -using osu.Game.Screens.Edit; using osu.Game.Skinning; using osu.Game.Stores; @@ -109,55 +108,73 @@ namespace osu.Game.Beatmaps } /// - /// Add a new difficulty to the beatmap set represented by the provided . + /// Add a new difficulty to the provided based on the provided . /// The new difficulty will be backed by a model /// and represented by the returned . /// - public virtual WorkingBeatmap CreateNewDifficulty(NewDifficultyCreationParameters creationParameters) + /// + /// Contrary to , this method does not preserve hitobjects and beatmap-level settings from . + /// The created beatmap will have zero hitobjects and will have default settings (including difficulty settings), but will preserve metadata and existing timing points. + /// + /// The to add the new difficulty to. + /// The to use as a baseline reference when creating the new difficulty. + /// The ruleset with which the new difficulty should be created. + public virtual WorkingBeatmap CreateNewDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap, RulesetInfo rulesetInfo) { - var referenceBeatmap = creationParameters.ReferenceBeatmap; - var targetBeatmapSet = creationParameters.BeatmapSet; + var playableBeatmap = referenceWorkingBeatmap.GetPlayableBeatmap(rulesetInfo); + var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), playableBeatmap.Metadata.DeepClone()); + var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; + foreach (var timingPoint in playableBeatmap.ControlPointInfo.TimingPoints) + newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); + + return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin); + } + + /// + /// Add a copy of the provided to the provided . + /// The new difficulty will be backed by a model + /// and represented by the returned . + /// + /// + /// Contrary to , this method creates a nearly-exact copy of + /// (with the exception of a few key properties that cannot be copied under any circumstance, like difficulty name, beatmap hash, or online status). + /// + /// The to add the copy to. + /// The to be copied. + public virtual WorkingBeatmap CopyExistingDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap) + { + var newBeatmap = referenceWorkingBeatmap.GetPlayableBeatmap(referenceWorkingBeatmap.BeatmapInfo.Ruleset).Clone(); BeatmapInfo newBeatmapInfo; - IBeatmap newBeatmap; - if (creationParameters.CreateBlank) - { - newBeatmapInfo = new BeatmapInfo(creationParameters.Ruleset, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); - newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; - foreach (var timingPoint in referenceBeatmap.ControlPointInfo.TimingPoints) - newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); - } - else - { - newBeatmap = referenceBeatmap.Clone(); - newBeatmap.BeatmapInfo = newBeatmapInfo = referenceBeatmap.BeatmapInfo.Clone(); - // assign a new ID to the clone. - newBeatmapInfo.ID = Guid.NewGuid(); - // add "(copy)" suffix to difficulty name to avoid clashes on save. - newBeatmapInfo.DifficultyName += " (copy)"; - // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. - newBeatmapInfo.Hash = string.Empty; - // clear online properties. - newBeatmapInfo.OnlineID = -1; - newBeatmapInfo.Status = BeatmapOnlineStatus.None; - } + newBeatmap.BeatmapInfo = newBeatmapInfo = referenceWorkingBeatmap.BeatmapInfo.Clone(); + // assign a new ID to the clone. + newBeatmapInfo.ID = Guid.NewGuid(); + // add "(copy)" suffix to difficulty name to avoid clashes on save. + newBeatmapInfo.DifficultyName += " (copy)"; + // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. + newBeatmapInfo.Hash = string.Empty; + // clear online properties. + newBeatmapInfo.OnlineID = -1; + newBeatmapInfo.Status = BeatmapOnlineStatus.None; + return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin); + } + + private WorkingBeatmap addDifficultyToSet(BeatmapSetInfo targetBeatmapSet, IBeatmap newBeatmap, ISkin beatmapSkin) + { // populate circular beatmap set info <-> beatmap info references manually. // several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()` // rely on them being freely traversable in both directions for correct operation. - targetBeatmapSet.Beatmaps.Add(newBeatmapInfo); - newBeatmapInfo.BeatmapSet = targetBeatmapSet; + targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo); + newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet; - beatmapModelManager.Save(newBeatmapInfo, newBeatmap, creationParameters.ReferenceBeatmapSkin); + beatmapModelManager.Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin); workingBeatmapCache.Invalidate(targetBeatmapSet); return GetWorkingBeatmap(newBeatmap.BeatmapInfo); } - // TODO: add back support for making a copy of another difficulty - // (likely via a separate `CopyDifficulty()` method). - /// /// Delete a beatmap difficulty. /// diff --git a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs index 138e13bda1..aa6ca280ee 100644 --- a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs +++ b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs @@ -10,11 +10,11 @@ namespace osu.Game.Screens.Edit { /// /// Delegate used to create new difficulties. - /// A value of in the clearAllObjects parameter - /// indicates that the new difficulty should have its hitobjects cleared; - /// otherwise, the new difficulty should be an exact copy of an existing one. + /// A value of in the createCopy parameter + /// indicates that the new difficulty should be an exact copy of an existing one; + /// otherwise, the new difficulty should have its hitobjects and beatmap-level settings cleared. /// - public delegate void CreateNewDifficulty(bool clearAllObjects); + public delegate void CreateNewDifficulty(bool createCopy); public CreateNewDifficultyDialog(CreateNewDifficulty createNewDifficulty) { @@ -27,12 +27,12 @@ namespace osu.Game.Screens.Edit new PopupDialogOkButton { Text = "Yeah, let's start from scratch!", - Action = () => createNewDifficulty.Invoke(true) + Action = () => createNewDifficulty.Invoke(false) }, new PopupDialogCancelButton { Text = "No, create an exact copy of this difficulty", - Action = () => createNewDifficulty.Invoke(false) + Action = () => createNewDifficulty.Invoke(true) }, new PopupDialogCancelButton { diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 7a3c4f2a19..c2775ae101 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -9,7 +9,6 @@ using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -359,14 +358,14 @@ namespace osu.Game.Screens.Edit /// /// Creates an instance representing the current state of the editor. /// - /// - /// The next beatmap to be shown, in the case of difficulty switch. + /// + /// The ruleset of the next beatmap to be shown, in the case of difficulty switch. /// indicates that the beatmap will not be changing. /// - public EditorState GetState([CanBeNull] BeatmapInfo nextBeatmap = null) => new EditorState + public EditorState GetState([CanBeNull] RulesetInfo nextRuleset = null) => new EditorState { Time = clock.CurrentTimeAccurate, - ClipboardContent = nextBeatmap == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextBeatmap.Ruleset.ShortName ? Clipboard.Content.Value : string.Empty + ClipboardContent = nextRuleset == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextRuleset.ShortName ? Clipboard.Content.Value : string.Empty }; /// @@ -845,23 +844,15 @@ namespace osu.Game.Screens.Edit { if (!rulesetInfo.Equals(editorBeatmap.BeatmapInfo.Ruleset)) { - switchToNewDifficulty(rulesetInfo, true); + switchToNewDifficulty(rulesetInfo, false); return; } - dialogOverlay.Push(new CreateNewDifficultyDialog(clearAllObjects => switchToNewDifficulty(rulesetInfo, clearAllObjects))); + dialogOverlay.Push(new CreateNewDifficultyDialog(createCopy => switchToNewDifficulty(rulesetInfo, createCopy))); } - private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool clearAllObjects) - => loader?.ScheduleSwitchToNewDifficulty(new NewDifficultyCreationParameters - ( - editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull(), - rulesetInfo, - editorBeatmap, - editorBeatmap.BeatmapSkin, - clearAllObjects, - GetState() - )); + private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool createCopy) + => loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo, rulesetInfo, createCopy, GetState(rulesetInfo)); private EditorMenuItem createDifficultySwitchMenu() { @@ -886,7 +877,7 @@ namespace osu.Game.Screens.Edit return new EditorMenuItem("Change difficulty") { Items = difficultyItems }; } - protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap)); + protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset)); private void cancelExit() { diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 505a57f157..0a2b8437fa 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -4,6 +4,7 @@ using System; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -11,6 +12,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -79,19 +81,18 @@ namespace osu.Game.Screens.Edit } } - public void ScheduleSwitchToNewDifficulty(NewDifficultyCreationParameters creationParameters) + public void ScheduleSwitchToNewDifficulty(BeatmapInfo referenceBeatmapInfo, RulesetInfo rulesetInfo, bool createCopy, EditorState editorState) => scheduleDifficultySwitch(() => { try { - var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(creationParameters.ReferenceBeatmap.BeatmapInfo); - return beatmapManager.CreateNewDifficulty(new NewDifficultyCreationParameters( - refetchedBeatmap.BeatmapSetInfo, - refetchedBeatmap.BeatmapInfo.Ruleset, - refetchedBeatmap.Beatmap, - refetchedBeatmap.Skin, - creationParameters.CreateBlank, - creationParameters.EditorState)); + // fetch a fresh detached reference from database to avoid polluting model instances attached to cached working beatmaps. + var targetBeatmapSet = beatmapManager.QueryBeatmap(b => b.ID == referenceBeatmapInfo.ID).AsNonNull().BeatmapSet.AsNonNull(); + var referenceWorkingBeatmap = beatmapManager.GetWorkingBeatmap(referenceBeatmapInfo); + + return createCopy + ? beatmapManager.CopyExistingDifficulty(targetBeatmapSet, referenceWorkingBeatmap) + : beatmapManager.CreateNewDifficulty(targetBeatmapSet, referenceWorkingBeatmap, rulesetInfo); } catch (Exception ex) { @@ -100,7 +101,7 @@ namespace osu.Game.Screens.Edit Logger.Error(ex, ex.Message); return Beatmap.Value; } - }, creationParameters.EditorState); + }, editorState); public void ScheduleSwitchToExistingDifficulty(BeatmapInfo beatmapInfo, EditorState editorState) => scheduleDifficultySwitch(() => beatmapManager.GetWorkingBeatmap(beatmapInfo), editorState); diff --git a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs deleted file mode 100644 index a6458a9456..0000000000 --- a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable enable - -using osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Skinning; - -namespace osu.Game.Screens.Edit -{ - public class NewDifficultyCreationParameters - { - /// - /// The that should contain the newly-created difficulty. - /// - public BeatmapSetInfo BeatmapSet { get; } - - /// - /// The that the new difficulty should be playable for. - /// - public RulesetInfo Ruleset { get; } - - /// - /// A reference upon which the new difficulty should be based. - /// - public IBeatmap ReferenceBeatmap { get; } - - /// - /// A reference that the new difficulty should base its own skin upon. - /// - public ISkin? ReferenceBeatmapSkin { get; } - - /// - /// Whether the new difficulty should be blank. - /// - /// - /// A blank difficulty will have no objects, no control points other than timing points taken from - /// and will not share values with , - /// but it will share metadata and timing information with . - /// - public bool CreateBlank { get; } - - /// - /// The saved state of the previous which should be restored upon opening the newly-created difficulty. - /// - public EditorState EditorState { get; } - - public NewDifficultyCreationParameters( - BeatmapSetInfo beatmapSet, - RulesetInfo ruleset, - IBeatmap referenceBeatmap, - ISkin? referenceBeatmapSkin, - bool createBlank, - EditorState editorState) - { - BeatmapSet = beatmapSet; - Ruleset = ruleset; - ReferenceBeatmap = referenceBeatmap; - ReferenceBeatmapSkin = referenceBeatmapSkin; - CreateBlank = createBlank; - EditorState = editorState; - } - } -} diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 8c8a106791..24015590e2 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -136,7 +136,13 @@ namespace osu.Game.Tests.Visual return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); } - public override WorkingBeatmap CreateNewDifficulty(NewDifficultyCreationParameters creationParameters) + public override WorkingBeatmap CreateNewDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap, RulesetInfo rulesetInfo) + { + // don't actually care about properly creating a difficulty for this context. + return TestBeatmap; + } + + public override WorkingBeatmap CopyExistingDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap) { // don't actually care about properly creating a difficulty for this context. return TestBeatmap; From f5d0eb41cb1041c999921ada5c271411de00513f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 15 Feb 2022 02:42:37 +0300 Subject: [PATCH 538/587] Update further `ChangeFocus` usages --- osu.Game/Overlays/Login/LoginPanel.cs | 3 ++- osu.Game/Overlays/LoginOverlay.cs | 2 +- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index d1e5bfe809..481abd48ab 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -183,7 +183,8 @@ namespace osu.Game.Overlays.Login break; } - if (form != null) GetContainingInputManager()?.ChangeFocus(form); + if (form != null) + ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form)); }); public override bool AcceptsFocus => true; diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index f3562aa6d9..9b2d7ca1ee 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -78,7 +78,7 @@ namespace osu.Game.Overlays panel.Bounding = true; this.FadeIn(transition_time, Easing.OutQuint); - GetContainingInputManager().ChangeFocus(panel); + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(panel)); } protected override void PopOut() diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index f0ca3e1bbc..571dfb3f6f 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Edit.Setup base.LoadComplete(); if (string.IsNullOrEmpty(ArtistTextBox.Current.Value)) - GetContainingInputManager().ChangeFocus(ArtistTextBox); + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(ArtistTextBox)); ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); From 60153bb69df75819e7e2830596884bbab8b04624 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 13:22:14 +0900 Subject: [PATCH 539/587] Update nuget packages to highest usable versions EF packages are intentionally pinned to 5.0.14 as higher versions no longer support `netstandard2.1`, which we require for xamarin projects. --- ...u.Game.Rulesets.EmptyFreeform.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- ....Game.Rulesets.EmptyScrolling.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- osu.Desktop/osu.Desktop.csproj | 9 +++++--- .../osu.Game.Benchmarks.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- .../osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- .../osu.Game.Tournament.Tests.csproj | 2 +- osu.Game/osu.Game.csproj | 22 +++++++++---------- 13 files changed, 28 insertions(+), 25 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index cb922c5a58..c305872288 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 5ecd9cc675..ec90885cd9 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index 33ad0ac4f7..f2e143a9c5 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 5ecd9cc675..ec90885cd9 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 5e203af1f2..b1117bf796 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,10 +26,13 @@ - + - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 434c0e0367..2bdb6a650c 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index fc6d900567..a6c614f22f 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index ddad2adfea..ff49d6d4dd 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index bd4c3d3345..a2e54f5cdc 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index a6b8eb8651..debddae037 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index acf1e8470b..40969c8b29 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index c7314a4969..9fd73f2c1b 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 847832f8ac..7b50c804ff 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,27 +18,27 @@ - + - - + + - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + From 28b45fa8999ba897fa93c0f4a4a5fc1e3700a71e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 13:40:58 +0900 Subject: [PATCH 540/587] Add assertions against null reference for connection usages --- .../Multiplayer/OnlineMultiplayerClient.cs | 29 +++++++++++++++++++ .../Online/Spectator/OnlineSpectatorClient.cs | 11 +++++++ 2 files changed, 40 insertions(+) diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 3794bec228..ad898759ff 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -4,6 +4,7 @@ #nullable enable using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -79,6 +80,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.FromCanceled(new CancellationToken(true)); + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty); } @@ -87,6 +90,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.FromCanceled(new CancellationToken(true)); + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); } @@ -95,6 +100,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId); } @@ -103,6 +110,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.KickUser), userId); } @@ -111,6 +120,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings); } @@ -119,6 +130,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState); } @@ -127,6 +140,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability); } @@ -135,6 +150,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserMods), newMods); } @@ -143,6 +160,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.SendMatchRequest), request); } @@ -151,6 +170,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); } @@ -159,6 +180,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplay)); } @@ -167,6 +190,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.AddPlaylistItem), item); } @@ -175,6 +200,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.EditPlaylistItem), item); } @@ -183,6 +210,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index 753796158e..ddde69c627 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; @@ -51,6 +52,8 @@ namespace osu.Game.Online.Spectator if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), state); } @@ -59,6 +62,8 @@ namespace osu.Game.Online.Spectator if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); } @@ -67,6 +72,8 @@ namespace osu.Game.Online.Spectator if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), state); } @@ -75,6 +82,8 @@ namespace osu.Game.Online.Spectator if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); } @@ -83,6 +92,8 @@ namespace osu.Game.Online.Spectator if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.SendAsync(nameof(ISpectatorServer.EndWatchingUser), userId); } } From 8ec28dc8bcc1ab691af17e3124b046a6a27a1037 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 13:41:10 +0900 Subject: [PATCH 541/587] Update `OsuDbContext` in line with EF changes --- osu.Game/Database/OsuDbContext.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 441b090a6e..79183b6f0e 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -3,7 +3,6 @@ using System; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using osu.Framework.Logging; using osu.Framework.Statistics; @@ -12,8 +11,9 @@ using osu.Game.Configuration; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Scoring; -using LogLevel = Microsoft.Extensions.Logging.LogLevel; using osu.Game.Skinning; +using SQLitePCL; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace osu.Game.Database { @@ -40,10 +40,10 @@ namespace osu.Game.Database static OsuDbContext() { // required to initialise native SQLite libraries on some platforms. - SQLitePCL.Batteries_V2.Init(); + Batteries_V2.Init(); // https://github.com/aspnet/EntityFrameworkCore/issues/9994#issuecomment-508588678 - SQLitePCL.raw.sqlite3_config(2 /*SQLITE_CONFIG_MULTITHREAD*/); + raw.sqlite3_config(2 /*SQLITE_CONFIG_MULTITHREAD*/); } /// @@ -116,7 +116,6 @@ namespace osu.Game.Database optionsBuilder // this is required for the time being due to the way we are querying in places like BeatmapStore. // if we ever move to having consumers file their own .Includes, or get eager loading support, this could be re-enabled. - .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning)) .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10)) .UseLoggerFactory(logger.Value); } From 334fe1f1203ac02f507132bd1d64f00139dfb81d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 15:23:14 +0900 Subject: [PATCH 542/587] Add `AsSplitQuery` specification to avoid optimisation recommendation log messages --- osu.Game/Database/EFToRealmMigrator.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 0bb5388d55..c9deee19fe 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -215,7 +215,8 @@ namespace osu.Game.Database .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) .Include(s => s.Files).ThenInclude(f => f.FileInfo) - .Include(s => s.Metadata); + .Include(s => s.Metadata) + .AsSplitQuery(); log("Beginning beatmaps migration to realm"); @@ -344,7 +345,8 @@ namespace osu.Game.Database .Include(s => s.Ruleset) .Include(s => s.BeatmapInfo) .Include(s => s.Files) - .ThenInclude(f => f.FileInfo); + .ThenInclude(f => f.FileInfo) + .AsSplitQuery(); log("Beginning scores migration to realm"); @@ -434,6 +436,7 @@ namespace osu.Game.Database var existingSkins = db.SkinInfo .Include(s => s.Files) .ThenInclude(f => f.FileInfo) + .AsSplitQuery() .ToList(); // previous entries in EF are removed post migration. From 2c3e50a450359d76c95fc4628b391733dc354f4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 15:46:25 +0900 Subject: [PATCH 543/587] Update package references in xamarin `props` files --- osu.Android.props | 2 +- osu.iOS.props | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 6b3142fbdc..eab2be1b72 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -56,6 +56,6 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 00c9653146..03a105673c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -79,15 +79,15 @@ - - - + + + - + - - + + From efeba30b9fe0976fd99c89841029323f88c2f243 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 16:01:14 +0900 Subject: [PATCH 544/587] Remove ruleset and mod bindables from PlaylistItem --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 2 +- .../Visual/Multiplayer/QueueModeTestScene.cs | 2 +- .../TestSceneDrawableRoomPlaylist.cs | 41 +++++++------- .../TestSceneMatchBeatmapDetailArea.cs | 11 ++-- .../Multiplayer/TestSceneMultiplayer.cs | 49 ++++++++-------- .../TestSceneMultiplayerMatchSubScreen.cs | 11 ++-- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 2 +- .../TestSceneMultiplayerPlaylist.cs | 4 +- .../TestSceneMultiplayerReadyButton.cs | 2 +- .../TestSceneMultiplayerSpectateButton.cs | 2 +- .../TestScenePlaylistsRoomSettingsPlaylist.cs | 11 ++-- .../TestScenePlaylistsSongSelect.cs | 19 ++++++- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 8 +-- .../TestScenePlaylistsResultsScreen.cs | 2 +- .../TestScenePlaylistsRoomCreation.cs | 8 +-- .../Online/Multiplayer/MultiplayerClient.cs | 34 ++++------- .../Online/Rooms/MultiplayerPlaylistItem.cs | 4 +- osu.Game/Online/Rooms/MultiplayerScore.cs | 6 +- osu.Game/Online/Rooms/PlaylistItem.cs | 56 ++----------------- .../OnlinePlay/Components/ModeTypeInfo.cs | 11 +++- .../OnlinePlay/Components/RoomManager.cs | 6 +- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 19 ++++--- .../Lounge/Components/RoomsContainer.cs | 3 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 +- .../Multiplayer/MultiplayerMatchSongSelect.cs | 5 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- .../OnlinePlay/OnlinePlaySongSelect.cs | 27 +++++---- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 6 +- .../Playlists/PlaylistsSongSelect.cs | 24 +++----- .../Multiplayer/MultiplayerTestScene.cs | 2 +- .../Visual/OnlinePlay/TestRoomManager.cs | 2 +- 31 files changed, 178 insertions(+), 207 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 343fc7e6e0..9aa04dda92 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Online selectedItem.Value = new PlaylistItem { Beatmap = { Value = testBeatmapInfo }, - Ruleset = { Value = testBeatmapInfo.Ruleset }, + RulesetID = testBeatmapInfo.Ruleset.OnlineID, }; Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index c79395b343..36d6c6a306 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = InitialBeatmap }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } })); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 5c8c90e166..659cc22350 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -215,25 +216,25 @@ namespace osu.Game.Tests.Visual.Multiplayer { ID = 0, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, Expired = true, - RequiredMods = + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }, new PlaylistItem { ID = 1, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } } } @@ -314,12 +315,12 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapSet = new BeatmapSetInfo() } }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } @@ -348,12 +349,12 @@ namespace osu.Game.Tests.Visual.Multiplayer ID = index++, OwnerID = 2, Beatmap = { Value = b }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 1d61a5d496..6144824ba0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -35,12 +36,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { ID = SelectedRoom.Value.Playlist.Count, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 8f6ba6375f..41715f6cfb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -19,6 +19,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -99,7 +100,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -235,7 +236,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -257,7 +258,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -287,7 +288,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -318,7 +319,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -340,7 +341,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -373,7 +374,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -393,7 +394,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -415,7 +416,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -454,7 +455,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -493,7 +494,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -532,7 +533,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -566,7 +567,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -606,7 +607,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -626,8 +627,8 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - AllowedMods = { new OsuModHidden() } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod(new OsuModHidden()) } } } }); @@ -666,7 +667,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -697,7 +698,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -715,7 +716,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { ID = 2, Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -743,7 +744,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -779,7 +780,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -818,7 +819,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -849,7 +850,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -882,7 +883,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 869fb17317..a6151198cf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -10,6 +10,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -72,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -89,8 +90,8 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new TaikoRuleset().RulesetInfo }, - AllowedMods = { new TaikoModSwap() } + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod(new TaikoModSwap()) } }); }); @@ -112,7 +113,7 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -127,7 +128,7 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 73f2ed5b39..010e9dc078 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem { Beatmap = { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset } + RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }, Client.Room?.Users.ToArray())); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 936798e6b4..361178bfe4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -146,12 +146,12 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value } + RulesetID = Ruleset.Value.OnlineID, }, new PlaylistItem { Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value }, + RulesetID = Ruleset.Value.OnlineID, Expired = true } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 9867e5225e..7834226f15 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Multiplayer selectedItem.Value = new PlaylistItem { Beatmap = { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }, + RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID }; if (button != null) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 42ae279667..70d4d9dd55 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer selectedItem.Value = new PlaylistItem { Beatmap = { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }, + RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }; Child = new FillFlowContainer diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index e63e58824f..8bfdda29d5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -161,12 +162,12 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapSet = new BeatmapSetInfo() } }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index d933491ab6..3333afc88b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -115,8 +115,17 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2); AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); - AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value)); - AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.Last().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item 1 has rate 1.5", () => + { + var mod = (OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + return Precision.AlmostEquals(1.5, mod.SpeedChange.Value); + }); + + AddAssert("item 2 has rate 2", () => + { + var mod = (OsuModDoubleTime)SelectedRoom.Value.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset()); + return Precision.AlmostEquals(2, mod.SpeedChange.Value); + }); } /// @@ -138,7 +147,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2); - AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item has rate 1.5", () => + { + var m = (OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + return Precision.AlmostEquals(1.5, m.SpeedChange.Value); + }); } private class TestPlaylistsSongSelect : PlaylistsSongSelect diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 781f0a1824..513c1413fa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 11df115b1a..a05d01613c 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -168,7 +168,7 @@ namespace osu.Game.Tests.Visual.Playlists LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID })); }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 68225f6d64..578ea63b4e 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Playlists room.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedBeatmap.Beatmaps.First() }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Playlists room.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedBeatmap.Beatmaps.First() }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual.Playlists room.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedBeatmap.Beatmaps.First() }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Playlists } } }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 903aaa89e3..9d45229961 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -727,30 +727,18 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); } - private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) + private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem { - var ruleset = Rulesets.GetRuleset(item.RulesetID); - - Debug.Assert(ruleset != null); - - var rulesetInstance = ruleset.CreateInstance(); - - var playlistItem = new PlaylistItem - { - ID = item.ID, - BeatmapID = item.BeatmapID, - OwnerID = item.OwnerID, - Ruleset = { Value = ruleset }, - Expired = item.Expired, - PlaylistOrder = item.PlaylistOrder, - PlayedAt = item.PlayedAt - }; - - playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); - playlistItem.AllowedMods.AddRange(item.AllowedMods.Select(m => m.ToMod(rulesetInstance))); - - return playlistItem; - } + ID = item.ID, + BeatmapID = item.BeatmapID, + OwnerID = item.OwnerID, + RulesetID = item.RulesetID, + Expired = item.Expired, + PlaylistOrder = item.PlaylistOrder, + PlayedAt = item.PlayedAt, + RequiredMods = item.RequiredMods.ToArray(), + AllowedMods = item.AllowedMods.ToArray() + }; /// /// Retrieves a from an online source. diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 8ec073ff1e..d74cdd8c34 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -66,8 +66,8 @@ namespace osu.Game.Online.Rooms BeatmapID = item.BeatmapID; BeatmapChecksum = item.Beatmap.Value?.MD5Hash ?? string.Empty; RulesetID = item.RulesetID; - RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(); - AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray(); + RequiredMods = item.RequiredMods.ToArray(); + AllowedMods = item.AllowedMods.ToArray(); Expired = item.Expired; PlaylistOrder = item.PlaylistOrder ?? 0; PlayedAt = item.PlayedAt; diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index f1bb57bd9d..85327be037 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -65,7 +65,11 @@ namespace osu.Game.Online.Rooms public ScoreInfo CreateScoreInfo(RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap) { - var rulesetInstance = playlistItem.Ruleset.Value.CreateInstance(); + var ruleset = rulesets.GetRuleset(playlistItem.RulesetID); + if (ruleset == null) + throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {playlistItem.RulesetID}"); + + var rulesetInstance = ruleset.CreateInstance(); var scoreInfo = new ScoreInfo { diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 83a70c405b..c082babb01 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; @@ -10,8 +9,6 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; namespace osu.Game.Online.Rooms { @@ -49,68 +46,25 @@ namespace osu.Game.Online.Rooms [JsonIgnore] public readonly Bindable Beatmap = new Bindable(); - [JsonIgnore] - public readonly Bindable Ruleset = new Bindable(); - - [JsonIgnore] - public readonly BindableList AllowedMods = new BindableList(); - - [JsonIgnore] - public readonly BindableList RequiredMods = new BindableList(); - [JsonProperty("beatmap")] private APIBeatmap apiBeatmap { get; set; } - private APIMod[] allowedModsBacking; - [JsonProperty("allowed_mods")] - private APIMod[] allowedMods - { - get => AllowedMods.Select(m => new APIMod(m)).ToArray(); - set => allowedModsBacking = value; - } - - private APIMod[] requiredModsBacking; + public APIMod[] AllowedMods { get; set; } = Array.Empty(); [JsonProperty("required_mods")] - private APIMod[] requiredMods - { - get => RequiredMods.Select(m => new APIMod(m)).ToArray(); - set => requiredModsBacking = value; - } + public APIMod[] RequiredMods { get; set; } = Array.Empty(); public PlaylistItem() { Beatmap.BindValueChanged(beatmap => BeatmapID = beatmap.NewValue?.OnlineID ?? -1); - Ruleset.BindValueChanged(ruleset => RulesetID = ruleset.NewValue?.OnlineID ?? 0); } public void MarkInvalid() => valid.Value = false; - public void MapObjects(IRulesetStore rulesets) + public void MapObjects() { Beatmap.Value ??= apiBeatmap; - Ruleset.Value ??= rulesets.GetRuleset(RulesetID); - - Debug.Assert(Ruleset.Value != null); - - Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); - - if (allowedModsBacking != null) - { - AllowedMods.Clear(); - AllowedMods.AddRange(allowedModsBacking.Select(m => m.ToMod(rulesetInstance))); - - allowedModsBacking = null; - } - - if (requiredModsBacking != null) - { - RequiredMods.Clear(); - RequiredMods.AddRange(requiredModsBacking.Select(m => m.ToMod(rulesetInstance))); - - requiredModsBacking = null; - } } #region Newtonsoft.Json implicit ShouldSerialize() methods @@ -133,7 +87,7 @@ namespace osu.Game.Online.Rooms && BeatmapID == other.BeatmapID && RulesetID == other.RulesetID && Expired == other.Expired - && allowedMods.SequenceEqual(other.allowedMods) - && requiredMods.SequenceEqual(other.requiredMods); + && AllowedMods.SequenceEqual(other.AllowedMods) + && RequiredMods.SequenceEqual(other.RequiredMods); } } diff --git a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs index 2026106c42..d534a1e374 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.Drawables; +using osu.Game.Rulesets; using osuTK; namespace osu.Game.Screens.OnlinePlay.Components @@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Components private const float height = 28; private const float transition_duration = 100; + [Resolved] + private RulesetStore rulesets { get; set; } + private Container drawableRuleset; public ModeTypeInfo() @@ -56,11 +60,14 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateBeatmap() { var item = Playlist.FirstOrDefault(); + var ruleset = item == null ? null : rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); - if (item?.Beatmap != null) + if (item?.Beatmap != null && ruleset != null) { + var mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray(); + drawableRuleset.FadeIn(transition_duration); - drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value, item.RequiredMods) { Size = new Vector2(height) }; + drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, ruleset.RulesetInfo, mods) { Size = new Vector2(height) }; } else drawableRuleset.FadeOut(transition_duration); diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 238aa4059d..21b64b61bb 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Rulesets; namespace osu.Game.Screens.OnlinePlay.Components { @@ -27,9 +26,6 @@ namespace osu.Game.Screens.OnlinePlay.Components protected IBindable JoinedRoom => joinedRoom; private readonly Bindable joinedRoom = new Bindable(); - [Resolved] - private IRulesetStore rulesets { get; set; } - [Resolved] private IAPIProvider api { get; set; } @@ -117,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Components try { foreach (var pi in room.Playlist) - pi.MapObjects(rulesets); + pi.MapObjects(); var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); if (existing == null) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index e1f7ea5e92..dcf2a5a480 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -69,8 +69,9 @@ namespace osu.Game.Screens.OnlinePlay private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; private readonly IBindable valid = new Bindable(); private readonly Bindable beatmap = new Bindable(); - private readonly Bindable ruleset = new Bindable(); - private readonly BindableList requiredMods = new BindableList(); + + private IRulesetInfo ruleset; + private Mod[] requiredMods; private Container maskingContainer; private Container difficultyIconContainer; @@ -86,6 +87,9 @@ namespace osu.Game.Screens.OnlinePlay private PanelBackground panelBackground; private FillFlowContainer mainFillFlow; + [Resolved] + private RulesetStore rulesets { get; set; } + [Resolved] private OsuColour colours { get; set; } @@ -108,8 +112,6 @@ namespace osu.Game.Screens.OnlinePlay beatmap.BindTo(item.Beatmap); valid.BindTo(item.Valid); - ruleset.BindTo(item.Ruleset); - requiredMods.BindTo(item.RequiredMods); if (item.Expired) Colour = OsuColour.Gray(0.5f); @@ -119,6 +121,11 @@ namespace osu.Game.Screens.OnlinePlay private void load() { maskingContainer.BorderColour = colours.Yellow; + + ruleset = rulesets.GetRuleset(Item.RulesetID); + var rulesetInstance = ruleset?.CreateInstance(); + + requiredMods = Item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } protected override void LoadComplete() @@ -145,9 +152,7 @@ namespace osu.Game.Screens.OnlinePlay }, true); beatmap.BindValueChanged(_ => Scheduler.AddOnce(refresh)); - ruleset.BindValueChanged(_ => Scheduler.AddOnce(refresh)); valid.BindValueChanged(_ => Scheduler.AddOnce(refresh)); - requiredMods.CollectionChanged += (_, __) => Scheduler.AddOnce(refresh); onScreenLoader.DelayedLoadStarted += _ => { @@ -276,7 +281,7 @@ namespace osu.Game.Screens.OnlinePlay } if (Item.Beatmap.Value != null) - difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; + difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; else difficultyIconContainer.Clear(); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index f4d7823fcc..9f917c978c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Extensions; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; @@ -78,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { bool matchingFilter = true; - matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.Ruleset.Value.MatchesOnlineID(criteria.Ruleset)); + matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.RulesetID == criteria.Ruleset.OnlineID); if (!string.IsNullOrEmpty(criteria.SearchString)) matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 2d5225639f..02e1b115a0 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -386,7 +386,9 @@ namespace osu.Game.Screens.OnlinePlay.Match if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList(); + var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + Debug.Assert(rulesetInstance != null); + Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } private void updateRuleset() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 073497e1ce..12caf1fde1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -11,7 +11,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -71,8 +70,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer BeatmapID = item.BeatmapID, BeatmapChecksum = item.Beatmap.Value.MD5Hash, RulesetID = item.RulesetID, - RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(), - AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray() + RequiredMods = item.RequiredMods.ToArray(), + AllowedMods = item.AllowedMods.ToArray() }; Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a397493bab..cb50a56052 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -247,7 +247,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // update local mods based on room's reported status for the local user (omitting the base call implementation). // this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed). var ruleset = Ruleset.Value.CreateInstance(); - Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList(); + Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(ruleset))).ToList(); } [Resolved(canBeNull: true)] diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 63957caee3..eab1f83967 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; @@ -37,6 +38,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] protected IBindable SelectedItem { get; private set; } + [Resolved] + private RulesetStore rulesets { get; set; } + protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); @@ -78,10 +82,15 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. - // Similarly, freeMods is currently empty but should only contain the allowed mods. - Mods.Value = SelectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); - FreeMods.Value = SelectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); + var rulesetInstance = SelectedItem?.Value?.RulesetID == null ? null : rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + + if (rulesetInstance != null) + { + // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. + // Similarly, freeMods is currently empty but should only contain the allowed mods. + Mods.Value = SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + FreeMods.Value = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); @@ -110,15 +119,11 @@ namespace osu.Game.Screens.OnlinePlay { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = - { - Value = Ruleset.Value - } + RulesetID = Ruleset.Value.OnlineID, + RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() }; - item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone())); - item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone())); - SelectItem(item); return true; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 35d417520e..2b071175d5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Game.Extensions; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -36,10 +37,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(PlaylistItem.Beatmap.Value)) throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap"); - if (!ruleset.Value.MatchesOnlineID(PlaylistItem.Ruleset.Value)) + if (ruleset.Value.OnlineID != PlaylistItem.RulesetID) throw new InvalidOperationException("Current Ruleset does not match PlaylistItem's Ruleset"); - if (!PlaylistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals))) + var localMods = Mods.Value.Select(m => new APIMod(m)).ToArray(); + if (!PlaylistItem.RequiredMods.All(m => localMods.Any(m.Equals))) throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods"); } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 0fd76f7e25..3ac576b18e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -3,6 +3,7 @@ using System.Linq; using osu.Framework.Screens; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.Select; @@ -30,7 +31,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists break; case 1: - populateItemFromCurrent(Playlist.Single()); + Playlist.Clear(); + createNewItem(); break; } @@ -41,24 +43,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { PlaylistItem item = new PlaylistItem { - ID = Playlist.Count == 0 ? 0 : Playlist.Max(p => p.ID) + 1 + ID = Playlist.Count == 0 ? 0 : Playlist.Max(p => p.ID) + 1, + Beatmap = { Value = Beatmap.Value.BeatmapInfo }, + RulesetID = Ruleset.Value.OnlineID, + RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() }; - populateItemFromCurrent(item); - Playlist.Add(item); } - - private void populateItemFromCurrent(PlaylistItem item) - { - item.Beatmap.Value = Beatmap.Value.BeatmapInfo; - item.Ruleset.Value = Ruleset.Value; - - item.RequiredMods.Clear(); - item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone())); - - item.AllowedMods.Clear(); - item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone())); - } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 7607122ef0..ed86d572b9 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value } + RulesetID = Ruleset.Value.OnlineID } } }; diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index 4cbc6174c9..73d0df2c36 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay { room.Playlist.Add(new PlaylistItem { - Ruleset = { Value = ruleset }, + RulesetID = ruleset.OnlineID, Beatmap = { Value = new BeatmapInfo From 5b765581d83b9d7613e507697d68392c034d8ebe Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 16:18:40 +0900 Subject: [PATCH 545/587] Fix free mod selection not showing allowed mods --- .../TestSceneMultiplayerMatchSubScreen.cs | 25 +++++++++++++++++++ .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 11 +++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index a6151198cf..4dd3427bee 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -14,11 +14,14 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; @@ -150,5 +153,27 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); } + + [Test] + public void TestFreeModSelectionHasAllowedMods() + { + AddStep("add playlist item with allowed mod", () => + { + SelectedRoom.Value.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod(new OsuModDoubleTime()) } + }); + }); + + ClickButtonWhenEnabled(); + + AddUntilStep("wait for join", () => RoomJoined); + + ClickButtonWhenEnabled(); + + AddAssert("mod select contains only double time mod", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Mod is OsuModDoubleTime); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 02e1b115a0..836629ada0 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -350,10 +351,12 @@ namespace osu.Game.Screens.OnlinePlay.Match if (selected == null) return; + var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + Debug.Assert(rulesetInstance != null); + var allowedMods = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)); + // Remove any user mods that are no longer allowed. - UserMods.Value = UserMods.Value - .Where(m => selected.AllowedMods.Any(a => m.GetType() == a.GetType())) - .ToList(); + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); UpdateMods(); updateRuleset(); @@ -367,7 +370,7 @@ namespace osu.Game.Screens.OnlinePlay.Match else { UserModsSection?.Show(); - userModsSelectOverlay.IsValidMod = m => selected.AllowedMods.Any(a => a.GetType() == m.GetType()); + userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } } From f03de16ee5a46deac3b5f2ca1edfba5c4c5dca7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 16:27:10 +0900 Subject: [PATCH 546/587] Add a test EF database Created on d8a23aad4 (just before skins were migrated to realm). This contains: - 2 beatmap sets (intro and disco prince) - 1 score (set on disco prince using autopilot/DT) - 1 skin (haxwell) - 322 named files (from skin) - 5 named files (from beatmaps) - 270 total file infos --- osu.Game.Tests/Resources/client.db | Bin 0 -> 266240 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Tests/Resources/client.db diff --git a/osu.Game.Tests/Resources/client.db b/osu.Game.Tests/Resources/client.db new file mode 100644 index 0000000000000000000000000000000000000000..079d5af3b7454bcd1c3acfdd199ce1a15c33567f GIT binary patch literal 266240 zcmeEv31D1R)&F~$Br|Vj-g~9crlpilQ%W1Uy?vn+NSkSiU1?IFBF4A2L(?QAlR{O* zf`Ed!FDRlY?zkX|Ac*3Eii+X_;szq%f+&ax3L^jCy>BL&NhU4G=lB1_o3!W5+~wSR zfA`#T&%Kj(rsu>BrE*k=2Zwii<$}&lbd;|m3j*^Lnb6 zd2adAEiK4R$$utgbonXyarw{Li-p%`E^qx7-y;8Qs;d8s=@%cLO}8yrz`tW%+4K9O z)sa`;?G1Gg#Dg_2b8EI1J9~?T-pKPNWywx2*50tG8uq_r}hx+Y9T9+Y6oBdN+4(LL(cCn|hZf0y{^; z(%^>33%ds8&+*Is#1~NGZY0SufM0fw|{C}ddgm~YeO_piUxwcjj=_M z;c>;7s?6YU6s{f|+_l>qCOAz*y0^4DB13)g;Nb4YB2J2iM@oa3jEPlGE``xZFdRh# zJ>|jSz5bv#98NV6wTonJ-6E+jnDLSHP1g9eo3|Fb*KMNfXmR_b{0`{e+TzyYrZvT$ zv6VY65bKvL-HyWMO@*%Fh9VaEn$Dgzon6IbMh@J$%6h5xwPyKD=-I^O)v#xk>d(Jh zRVD7Lz93px-=mtGhJC5#H|WgPBJ5iCnu@L34@gy&heheDzMj;P@r?R@RMSzpK_4pf zl ztx<)VmqPt?*5p86p8Z_b)jQBccJA>?{lrz6%AU1%50KrjT=L4WjbtyKY#%pyyHS74 zYGSyrOsCuC&*#ruLx;OQ+Pk_$Rb~8Drhc&F8P92~wbc$d)PRg&%5g$egI%KvW@9Bh z$r~;Kn;Mx=Pz--{Yr1XjT>jjX>G0Q<`Xd5dm8a{6UQK>Z10Sz+pkX)m3bsaZG>jcL zqV7-404h5;?2y)yZdK(1a+Fa zRBtKR)u2IGl5Kpt&9?b-yD9p8eZ{pKOFM?CEgm@mY7cj`VPaGLEHo@LHWOnayRn-l zN-gJ#Vx;NZV4G zMz0*~+|uvu^>HvG{&PbaD`%&A5*dzWsE(=VCXq3A?~f~?G8$~$fi<&IK~mXN4aL$d z-6fSf=?Sqr?5%#TQauPa?7?wusZ*;$RK-$kL#Ly_*eBhtG!)P|Gu4*}FjP%3Oj}U^ zUpyR*>^y0(e{^?Lxn3FR+<~J@LlN$b)Mg?=Q5DtIG(hQ)J`{&=0v;OKd$Kn?01z5h zJ?4q`IY;M0*U2RPTb8fJK;oQg>|rpaczj zp-)S#NyM0%iK&AM^%0%juhLj@b;ug21k|b0Y=d%lcO?xsY|NGD9I1|~>dM=;jYe5djb)r<|?MC zVz^2BxJ|(|3BhU2AZ|R#6+pRhH&&!mE0d9HP)=Afm5!W1-ME6;np&4szyd&vnnI^_ z<7m0MZX5P>MN73ERfF--bwlafui72koZ3#>v0)=zMF&6%os!DubWnpHTx*rdZEeE_ zyHlybq&_AF8f5F#*j83*axdTAl>jvkQbSQIo0569Ved^SmXWCA#?r_LtV}mK_0yZx zhLsIIQ7u$PKMdGir>}dUge&O&HTV#83NxR@el;g|2PgkZzFt08K3QHV&z7E+z9(HF zoh6+t>C!auLGhd7N5oNay|_64O#Y|&tMcdMH|H1S{)TGohatcaUjYn@sw%yu1(+t$w!m6qz}xl}RrrUrD6rr`9ETQ{6taj@6!g(a>^KYCEa@ zEbbJV&D3C0^toIgq$>Cw=%%1)UaBb2j!bbWAj;R_Z}VTr$q&j`$gfL&|9_h=);>dk zA;1t|2rvW~0t^9$07HNwzz|>vFa%z(2+U~Ba6Ep}d~0)S>v50A^rvY?OT%QqplA3@ zh8J3z@OlxciMlOKX*|Bqac%S+pK57JjY&F$uSgPF@Ka3|vFa#I^3;~7!Lx3T`5MT%}1Q-JK2pp78Eo>ss zDO96}@TP-lUo~qdB#LpA_Vwb!H}|R+I5iLbbGj2AlIjYF;9Q zJbEIhQwypk$@`XrTT)A^3Dw7@G8{L(HPul~CJ%w2Xo}0_QgemsLo9L=Bvk{lkn9|v znwzOU5+O<#am`XQCx1=8Kw2l^W%64O0ei;~U#EBWV9nZ)LAmLDW)yo$!=e*{kgERpV)F3&$IorGuo z!~ERbow+yWPRQ}u8?wXMCBpr}<-$oBH}kE`K0!`Dn!YL>rsuZa+4{!T6I%I}8(KzM zmNq}o{NCo1o2R92O`V%s+4NY`C!6A?dHh}coB0>_w<7e|H{I8Njt5{<7prXJX8=m);0TB;VQMVE?Ebj>v* zQ&Vi+b$lnZ^~iTLKh)IFveZabV>NOj-3S!RvK%u&A3Zm;Ayf-I&(Mnw72|4#;;F6^ zy1E-iu4cNvVVa)psgCa%u>(nlquXxen@$*_9o6xa$W%<*_5;ONiZ&Hv=)UUtjvtvZ zD(IoF`o3p5f#P_s8z~M1S)OBt2xAhO9(n2~LVg4=5hlcQjG~ncDz;H3a7`l!BFBIj zV4*mW9lBoNJ9v(X;A5F_XgR(gMWKfWgaRiD1N170iY65UsDj9F70=W3*fAo60WmRa zj&BB0i2eh5Jq$zDx3x%z5Je9Gt%G@W;!uywqCv%&wxb((EXVg<$F-5EMv-Qq1CAGX zw(ACwijf9>V1|$z1e#`hfu+O>!stY?;}>-*#*N)r^KCUYW8YL`UkwdAHbTvdP0NKW zKP2$#ii;<@pc2048fIiWwh|kPjwXv56=RsXW1EqytDfoDks2$ir>g-T3c_rJp63B( zL&cRVpS{G0~1`s4`l(xvrCX7LNhvB+j2Z-Z7G(IHB($c#YBM{8I}^5w&CfRK+6Wenr+2)9QqLg`arlAyB3L-0>x1sEpmM; z3JeXZ6!}4MITfQ@P&muPY*~@7TA>a8WG`J?OBn#8}7<)EAoOgorbRxw0%*)0G$ulL{S*J=6A}sXFw-(uzx| z7$9iscHrBt76$=3V?wty4{KSoFw>YtOsyT+kna0o=!BkSYGDw27Q*IurdwQ+49bpm z{F*>IK$lDlF=K)NPY^~%;9x)+R;X-HR;Xx^q82--80cN7Mu88Q10DS| zHK?fJhq^=78s^KiG!;{VVmAm$(;8I7hiS2(^lFHK z`Iha)ZUn8>0@yUgb4{!igfy~r*ca$$@yLo8AFB={^Aybpyf8#Fo`HUNo(e0ddss$> zX9cbv#Ia&Uum)J_y5~6_R*Pa37g8|>N&`|19>SoD#3~zxO?C#79AZH$E}cV6HX0cmx#_X%_5+s|3&%MEC*m!x%}HYx%JsVDXzUNwMXGF)+7bZ=rv- z>-jJYM&ww<1yqcN2u3iFk!^*6x)=xu&m}u-7`YUU&3BF%4q2+`zU11*(Qc2goXb8e+HeuuCX*@o*}}4_sF#76iMM z5u+iP2OHBzR85II>|3$pV5wr)MyDOvSWKabUfV#}@||LViorG#LBX*Bz;cBKx(gc- zn~FiY;dq)K=`K-P&%qo=8s-j`9g41Lp^07DE*?h37=9eu2G+I@VAa6D%wxL>VD8M& zfnCNj#tso!Dh#V>LLIyq3KnZvJ#a*fusA0X0|oX24VDagVq)KjP({(<=6IMOG^#*x zFfKR|fE|7UEFx@}5@-Zb)h`}O#b9+{TfhdPnRWyl2^1U^%Owm9Y`X@w6Aj_AVgf46 zn~#kP?uL(!#AE@)VR1GU<6>hb1I3OK#hMvmxnm>4(m@xX3-Cuw1F@@MEL6*a1tCrt zQ7PR~qR4UFwX=vA-gVUgdo4!os4;9X42R{zEzo?OctejyF3JQJ=AAjPtSw*YHl=(eQ>ii_uR^X8!FdrO22Q~5Vy{V`$%4klv9kqA}6%+Z` zmMu6CXapwTQgsb}o9Dr-z?E=S7*)KE03Eh&4YmVYxC;Y~fMK2u6J9)+iXj7qzXOv1 z6?Ijp9neutJ5?JU?eb6nu(Da@O|KrSPs@9aWb&Unoe;V6@!)L#R1$bAG(a4E>_U72%8$#VW?pT zPt1jD`@Rx6umh%P`i^O<@Hb%X^|cZa!>jNCV51zU4t!f66{{Fp;99U6@RrF&fvA3V zsA4IYu?hbO))a;i4xZ(}b4?YqNq68<*>F7IHy9dRmJqS(f*6+GaE$<+$MS#%!exi| zZ235ESV641umSL%U_6wfP#G1%d}61AF2K`rjnK1kWN`Ed8=s0E1=#4Y_rgOVLFOFt zLpDG7y&=LwdTudO=?$C&2W|#-`oM+uz-z*>1_Ls&W<9uru=T_vgWB0fXkhJYh(QQH z34KQs#WWQ|_E|XYSj@!N_hV?O4`k4&?h#`fwbZl&ju&V)9salj1 zF~m7gEm%Cb8u0HN3p8|e4=8$pf)p448|es}7Xoa|mmkBmMtN9|z%Pna44JzK`&00UHSq6Nf1qn|-W%K6+-kMjTrqnKvXG zp0B_%;=FzIzL$YI`#M0)VGv^_iT{r;{bN520fqoWfFZyTU316@w34>8FhwaO&o7g&LZqapDUmsQa_KeXi&K|V zx*wS|pU4bI@pyQ&R37Q#S~=krGY+0Cb2EC5|ND`LTvVHn|65a24tJwBQ1X^jx*t(A zp9tGcic=!W3%MrgdQN_;bUhgRVF)k;7y=9dh5$o=A;1t|2rvW~0t^9$!2c-(cBQgh z29I$F8DS%NcI5dyJCVlsL~6?vZJBBo6#HmRJ=#>4<1rgNRk%b^j#dWnrBjb?+JhOtQVnRJg z?U~e?<}5c|JS1FcZ5L06CU643@%cU_@brYnl%q{;xvJ`hWhFxs^Y7uBayN7Gs@%=V z@BgW3W}RUOFa#I^3;~7!Lx3T`5MT%}1Zoi2x4mT^hi?MIK?uj8OlBj`pP9*|(_ChD zMo6bJnM`Zy`JRvPTtA{}xTc422_YV)4@?B}2{k;%AK;m9$Iq;jYkGhE*qZiXMAgAHVQ|e&h$w=%4=$dLjv_oCj}R>uqOTx=Pk>k}h(myQ6o~1A zNGu4~VcUoVfv2^@03jw2QvgAj0uomU;cgs+2SKa>#9F~a@jBwuc($YIcv!rZPazT= z26ce=bA`o8oH~rLdtk&Hi0a2U?e*9F`kt?JCt1e_ujbb+dUUyRFvsy3E&>PO+3y%pt`K5GL3ADsKfZwo zVm^7m9)SrE=mT+C49f!+h;4>v)Jae#JPO~!H$B%8p5P6K_2bLF#{TQRbMGM;)zR_C zo{QPNXaB=%YEm5pb1|GCj1j%TCqZNIz`PzJb_@+VhLbhIW>_X-c_5MzLJqkhB5NT8 z0Sagcy@OCU2vwxTi29-d5k!*G5c&bZk`UR!CSiD*dH%T$ujq~Jtsh>hcv6#*pGPuk zhPUr{9*a-l1wr7G#m8-@vnRA=c%q(}RP*ya!6RS-;_sM<@1xo@UXmRnj28kW7>Ejj z*hfuClt3uxSU|w>)karc-AIo6%Af!7+M6FahUPrYT_H*jaxKz@ocxIVRq5x_Ez*V6 z-x?#sUNQt20t^9$07HNwzz|>vFa#I^3;~7!L*Rb~fnyK_5jR&U;a|E*It0NFd45qU zQ}-1GB4Qcx{OlAy-Y4Pn(b2njeqXAo^|@XL)ZFJl$Xj{-oTirNDr4-%1QJdp5}Kc@ zgt1ErNRTnErd0Cv2ul%9P~f*e%Xx45pjwXkV(Z(-B64I2t;HgD?b-P(!tLi?8WeRcJ4eYml6>-NI> z;`T!4w%*P7u8FP1jm1s9OA~>eMF%?DH!c9?`_nDYQe}W zca`>(Mh1tc7KSpry`joiE$&Cyn!(Y5ayWSA)cxuzjg&onl||2x7nBBev=;#1hA~(w zeOfnM3VTW~kEZO<3Es%g_Cjy*lmyL6wG&E+yg7B#8=`VK8ukaz>K<4<9C^D&rWC)i z%WT|2^*bo*Ai#81IhF-7^#gC9l7yytjX9 zTzbl0uxmp!P>KeEy^XO&k>PR0n5qoETqImQIJgU6eL`@Wh;(mhcSMHz;=#e)jYXUk z4Ud!tF&Pu9o?Hr}kzhEA271ba!+ZTfT%I?t#gCJg~XLV)hqN-hwYwOlyb#0D=);D=^t=+t}*u8EOU9yYYSD!c3 zl5ShFguk$@)^t?0r?S|qZ(AFhkZSI~Vn)X69?+Bwm$4ux&r&B%-!@PR21BUZ#7U$U zhe zr@9ru&!~AR)ay)5&T~?jx_SrFmd-t1sh_w$Q!Yq2iP+7{C9e!yPduaQjEw8WCOn~9 zlS-CZ4^~roLVCx(SEkc#^XK#Dt)VEakM^!EQ8gWZm8r*IJmWb*P+RSQ2n1wcQw|@Z z8rYpG#*G)iN%%4`Kvp9YSYr-c(T7Y|^iFGbCcb6=54x2;&gpTDqH4aWVaiXx0BG}prS==b#%*KREB z7^WtA8Cwd2;b>5vaB>(kF5OM7 zZKv5hhg~5$tD<|}XnBynZ`?|s+OY8yiT{`9ujlyd#jlD+{=@ktxzFZ=>>0wZgi|t) zW%j1ukv_C_q~+cgzxju$`}p7H?#OM-4z>1GZwCHHdDCs&fIT07N2Gv*D zEXGsjR_3UCQ&;g6TrD&z-A=rW_VGHHv-aw)(@~n8ZZpxE(4e(Wy4#IcJZyiBC50Qc zRmthtJ1gDRIhWrzzuHuF1L~{aNvg?nCN)>hsaCk;*evyw-ktaem6gtJWa@nF`fCGr zBg1ZFlEd=^yE^Wn|KGcjDYO@daJeuvviD?fc%b1?b^PRw1I3tu41x( z)^59vZ?GnuZ!2#npK2?1bv(ae&nnqucbRaseU9C#o)7-L=j;Q5R6Vxg6x&yQJ?X$d zo>7lUHJu)TrvfDVZ`cyYPGe-xukM#*PU`pA_71---L?$7aeC5jOip{%nuk{Q;BA|_ zPuy0-#}-vLqgtRZIU7!Fjiz?&YnhpDD|YbvI;xG-t5VGj*JsS0wDs5L*HlbAj{0me z-l`T6UEC)fl5Sg%ewb7AqwXH8URK+Ix>8qns@~KW>ex5q;B?zYmEY%1?$8*r)fe4e z<2|aT&D>9~s%84BFUX1}6Es;20jj1{$G%0=({0<)uM_vvuW>QN(`wYa8PBG76UFSW zkCV!F>^UgiwrT;tZ}xA|%9_F!VghW(gS2))VguQwAm zvzp$>`|r(!ij8|Su~5frR_4-es>;6>LF~pqo5hw?)$Z{DR8#Vme$;vFa#I^3;~7!Lx3T`5MT%}1Q-Ggf&V20T2egU+JeTW zH#dXkQz_8arlwR%5ZL+ue`yj}GYkQS07HNwzz|>vFa#I^3;~7!Lx3T`5coGEK+gXu zdj4;s=l`a<^ZzJ5|6jq$H_9u(*bhU1A;1t|2rvW~0t^9$07HNwzz|>vFa#I^99L|{ z*8o1>@6#a1@3s8%{UX7pF7!oszF#kpl^@6X|Ig&_|No4a?1v%15MT%}1Q-Gg0fqoW zfFZyTUMe;+45CEu6)z7Ph<#=sC@2rvW~ z0t^9$07HNwzz|>vFa#I^41xbT1jwKN;|mh;_UC1e#MBp4F z`F{qV|KG#4$hVR6{}b|0vFa#I^3;~9~ z3kv}n=ue~m|KBC&|HtLKvFa#I^3;~9~3k3mkAHhf!)b=}iUv=_yiYx|*N zIa+APp6xa*NNp4}J9KT!v{X&C70)$QJ61e1a(u&5^;ofz@Be#*eE$E4{K(YfV>t`~ zh5$o=A;1t|2rvW~0t^9$07HNwzz|>v{O2G*{_;OfzW^XbpZ{;d2mUQmkqls^e693T zsVKcsUXc2G`oiY(TTT|1G|8#m@{O%erEe3?$o-c4jBuTFWz)T_9hvvF9K=1*{GHs4 z?DwTMepUV%{??ZJvaidXnq8AxB#(+k@dx6?`7a1hw_M(IS>}Pv_WZJRr@SKf{`^Su zajDP#%ZL@5_ldj_IjUhOapd|z?0TjX21aO^wqh%muB%Gq*me|IVeI>nVaH||gm$QC zu3>AAu6g<~RElk?n&sQ>R^j1-bJ+>yhu^8c9<_%Tgm%jn&ABbR$qK%W}*BJ@nkrhD6jw78Pj#IT*HB^PYNqQOrs>(9>iC`!I}l_zy6r|jAPdoq>Uc_IDyD7YR>@Z! zD#g%!)$<)cGGmn2LtpiM&vF9A@mx1j9LTXe#|#z44o%NdF$&~I08&*|&oS&|M6rz` zfomE;5IF{<00+f^=+N~7-?0*GEHe%*2lEt#o}xN|6NLf#6hs!40+fQta23zf^w=>X z1=TQLj&BB0h|U9bJq$w?H(`+u8HyePRR=Tb#GxLUCY55^xJFZTjKg(Y8+mFJX$Gdo z@dDhGxk032m;r9=LWm6lO|!kgQes6h>)GFjNH!qdFSatrZ1^1{I3@ zUAoeW9Fg(*;nvBSfb^MwDIY5U@OE)kV zimpUP;9xWw7Nu<&o{dXcSMjk@Y#0F@rohxx!&a71DV_(#bk)ekXmzq8J&W{T^CBxy zfrz8U#LmPn^wD%f1vkJRaQ0mb7B5oNqo@?9Tc}2X4}=39oisHlrs3m$nXE6&lxb(#j-f6{Mg%heqX|n4z13paeXPJ3 zM$vZ7P! zDy#v_Y#g`>`Fz1pv0$K>fgamX1w(b_QzF}G1KSD|tQnXB&o;5n4HIo44?V=r zGM7p*{5Z4?EM^~&s)2zS$F3B>wwa*=6O46>tst;e7*W%N8hH3{2WuWH2A+ly9+pUf z*7|`4vjz1qu|Gtpq3G~pJj@J0RG=>y6ubm<51s(bA1syY-Va$B&Pw*#vbR#BOABTrhDK7S2GHuv7@IjOrVSQt} z!fHVWfEfG@(@<;`#zD0#SPbG}5lzw^C5jx!ot;Q=)d2e`M(n7u=W8$&mXD2J^L66u zV16~sssmFEcS?t$vG5;lnXrboX`8dC6!*#%FN8iO=rZ5-IR#d?2Z4CVU_4<4%nIEhajH6=TD5GU2EIOe{3G z127JTZd+=gxQ?pCravHV{O5_>cm5g4yIDD z?krf8keF=^+Yt;{5F4%oBdn=$2!{b`q#N+ceI?K>EDHD$rVrhO$%z$bdSyg#k1zwU z70^VyP<>rjG~a_01S6ne7_dj0iD4P=Y~XKL4vYzLBCwH~&OuZPtc4c`aG`uCFt)Q; zLAN68T(BjfhCMnl5BOZv$C&^N-^9nVj%nkgRUc+upGKuXPhfB@tUqjZa4j4QG<0+i zrp5~tq(CLfN3sfBA#BnR<;QU7P#)?W_;S)0cuO|i3b+M^27e?}Rh$xF(GAxK&~dB? z=pDRpxWpEYEuj^}s*5!b&j{8}$yP?Bz<9%SM;e?c*9biuX8=c!u(_$8i~Ti@vAe?m zP_fZCswxDdbflr{} zi#Z178S0~=g-n7Ac2ca|i*Ku^gI*m0wgu7xI7W#_)$i_AjdgwEb4i+{tErRE0+4%I^)y{#WVj3c=MQR zXa*KQsB^bv#ZU55Hr<+iC;bM1yYU?WKbF5Qe_6gkzE-|kzDoYE{C@cz@|)$?%NNRL z%V){w;u(RRav;A{-Y)mbo8=eDMfrGnrEJNnyi{H!x65o z>Xg4ODbf<@NNK)wnA9dsmqaNerNn=Ve-|GU9~K`J9}s^f{#?9MepLL9_zm$I@uT9E z;(Nt+iEk6%C|)Ft+}t|ZpvMk`%Lbt+=p`S%e^c2*4!nz3v>H&ugHz&26LrckUK4Ra_+?3`dl%0 zT+Yquxn;RUx%s&{xfwY*C*)Gur?XFF|D63p_JQm@*`H>Al)Wwc&FojQH)gNRemeWH z?3LNevv1B`oIO8#PIhm0B)cPfYIa+8bGAFXCc7$YXVvVI?85B4?Ck8pSuvZIhqJrI zo5dTlp0rYaNHXQ$NL!?9rSFNi%eP8DmVPE*CjAQP#(o$A3;~7!Lx3T`5P0Ds(2^3G z`011$MCmk2WlAMVMN0FO<|xflihxt7i-1$0X-ZouZK1T8hMGE)zD~kUjnLOkl=75v zgmTYN`cF!qZb@aDxPQ>sPf_|LrGKaNZEo3CmD0y3{R^d!Qu=2~AEESNO8-RZ zA1Qr^(mzo8Af>;j^mmm0meSu)`T(W(Q+gkzzoztFN`FP^J(S)}=`Sh$1*Jcy^k{RyRaQhEoaKc@6Yl>U&?A5eNbrQfIYdz9Wr>31pp4yE6w^j1p0Md>#w{RXAC zQ2KRBzeed-Dg6qiH&gm$O20(uO_Y9-(iJlzx`dYbgB; zrB_q>X-Ypu=_e`u1f^F|`f*A>M(IZ>{RpKWru0LUevr~DDg6MY@2B(%O5aE6dnvt~ z()Un$8Kv*0^j(x*O6fZ(eFvp)r}S-1!#ykkSh%J)hFoPRJvX_vvq;GRAXOdTY z$SV@VZIse7NgF2Q4D!AuM_vvRGC;^~Li!2WMab!dlnB{L$PPkcLLx#!LIOg3LOeoV zMo1qaFD2wO(nw0CG*1#vCFI3~Y^RCa$jg%nIf;-{2FJA%_t%hmb=FnN7Psi@agbr6C zFSCRQgk%UIK>$f+Y8s^?NgE<0iT_XbvT1~{ABF%!fFZyTUvFa#I^3;~7!Lx3T`5TFQ<^MAAaCr*As z{u90d@F@lhLx3T`5MT%}1Q-Gg0fqoWfFZyTU675G2pNw6hYsRqBSvi;Bgm;`nFv^l(98&; z?1qRCj-ZPupdp~J($s|rfC7nijBvMz|BG0qh@g$C2#Kg6b~EDoB8<9&h=AxP0)is+ zBSQZf7`u;H(iVawBIK=^m4C}M$@h})|GQWIE&2Tq)e_cih5$o=A;1t|2rvW~0t^9$ z07HNwzz|>v{5K;&g8r>1w;wb36uG{T5cdx(jUa@n6elEq|NnDNzF&S^epvqbe=}5U z>vFa#I^3;~7!Lx3T`5cm&7Ad_kn%3f*LU^X?IkB3J~<&mb=)WMm- zkx?Pl#_#q9N?wY^|N9S|S=MES07HNwzz|>vFa#I^3;~7!Lx3T`5MT&AM+9ol|8t*X zRaTrKzz|>vFa#I^3;~7!Lx3T`5MT%}1Q-Ggf&UN$MD&xFcXRm1ei#A_0fqoWfFZyT zUx4H+mAgL!2>;CGdvG} z=D&QMw>w%+`lg=3|++X3CjUGHzyW`swst>8sOkPLHOyr;kk^krrAXX}z`e zs@4lzPj5Y;wWBrN@=(hiEnjJQf6Km>m$s~FQCbdfNjE>-d}s5Qnm^opVRL`;wr02a z@aC4(L#ZF7u1{T&IxqFo)G?{Unx1I7qvjmzPk*Gfk%B8`9WxEH%WpTeZva`RmV`uqz?X2ZP13TvLYem6MfxEN5;7+eU zE|)MxD09TV7L-{ha6f5WW=vFbBI>S*#fHP-!C+S+DwT-(Y2z{#QSJMhP;8CB{d_{P zQW!=9W9{)td-v5B8jkwCv&IB*iJ)K9ml+@s1cSqLd<)M(nQnpmb_A5DxBMHn=Amrp=s{H1qd{B`VF#ITM9UfqQadAtGVs z9%L>N_}s`4R&g}EYeU%`0d&;JUBWKlA1I7A#mBaKvIgP??N@8BUY=G2Zu;{ z+v!NhNTo`;KR7TtviwYM&&*O%L9S#_LA!_a89Ql@XVoU7__Q4;K2PAMjg-A%I<$jg zq|6uigGSCA930t6WX_I|gsEt)R9@x}mdk^5mga}Z>J<3qYL+)NG(6}9JI811@Bl@i zWKT_;{i+iB=-Dbk9_cTI(J<*pIjLO9pYEal0)cxvDd3ag&v+TqIt1>Ym9$FH*?kbD z3sRCA4TR&pD7+L|os+VN9`}2DiOjjDL8dEk&m^_u!G0)DjewUR`#6E;CTCY#pK~gj zu!Ve5pk^RMdi#r!Sro)cnKeB)Y&*)h!kHt3ad}x%@XVd1kuvlVv#{_Km!&!YIwEbc#$NsdngAw1u^ks>v*uV7!qx_eA6Y=O2#i1KCp|Z-h?6kz0_B zE$0XI+2P=*-ye~^q%zJ0n^6u1?)It55!4UgghJ50D`B?a73^5nbHcI_46VN+!`Gk1Gu1%tc&!D{}> zB>$7u{8Q>xVeZk$M_1oi&0kZeDRbPU?blYby&xDJhPO$2e7KXye%-{(O52Cp$Ol9h zRr6QZKx8HDUOXYMQuPoM*?{QvW7!lU1F3-MlCjikOM24M8>$(VQIJZ9Xo;vdjpdQl zIVw^C>ZR4xDsHqLC28l))yy^3c2*?qyrr7aRWqXH$ODpZosd@@(K2LXL~k3*rY$W^ zT6+6fYNhzBCCGp#zoVM5W7r$oNqc@2a?ta4*5y>H&FM%oKT*xBPB*DJV=*$ZXg)Qb zNs`+aAsO!C_v=kcP#TDKdlk8DN1_an|JF#wA0F~XMo8sD7a{|5{Phvy5c=NmvS6@( zaFqHbbB{nC5V>VSUPb1N1+b*Vo2b^@Rifi-N3va)9C`yg636SX`N(t|WYSH%t&sHc z8`WM`oy$Y#CB6J+ZAPUVhaR5f-CE5fL#yP?oQu3A0{7LiKKeUIA7>txq<_0ohqePF zKXXo!{+&uXMG5J14y9w7l_0zPsavOvW>N1`4PG_qCdFR_aba1X=Jy zzCyE>(R0tNgOLX1{PegYRZCA#N?(_h)`{?G2O({qz+F8Yc_B3%2Tw!FLV^1XO{r9E zlaUA&yJk3ocQOLsb~y2350#Js75i*$MzuW=dFb8elJ*qR-t>Huc5RZT(zINX_W2}@ zYRmL&l6HNPW{`5T1f*ePHz#S>7lUYko-C$klG0yDa!e|foDGt?@$p`<(?MsU-_j1$th!g&bGdQ1lar#^7?$$5xkvJp&j(l70Q@LZZ zU(dd*Y2?|Ri2wCc{2$x!hX}uGTe5_|kS}|Ff6Kl6&)VkBt-OnRqH-A?%1D)KYql0U zdy9qM&ea==h4yN4yO=H1{45r;Le1MkcUPf%Q*Uuyacf~|qaBgm4nvCVV5*b-U zZeY3x;=w9EV!~J|+3khJbTIARUF~Dt-n6;5uxZ`?E-kd5fa{K$B&2i$LvNs5^2nVC8EQ>-#|68VrQ7Ds<1bm5ph3(!g`@gn z$pk#A>CX+2@mdE$WGlSiDaYG8SoZpRh_ES(bXI(w_Ci;24Zz>9SY2v#6sl8I0dC8d z_5yTv1D(D~+D7lJHN**1LepDM63`c6PucU4pi5krRYUssoZ-`9A zek5)l=*Nan0jVnmovcXj^2%cxQqk5*PEQ#MF@lp$gFa1EqC{B_q?3hqGM2_2sa+v! zu|lc}(N{Cj+Sf$N+Ra;w-Rm~d#k07*rXVsqFnw!_TZ^046nhG_dC*I;Mmq|dH$ek7 z6k(RuboQ+2>?$VXp0Xp=`2*s8)z_0cF`iN1foeKkT~l=++aVvs-Q~{X_Z?Ik2&1#Q zPpQG7Dxqr(8nv?Wx;J$dPr)*7Sh}5zvwf@%8U2`~_KtnLmTv1rgDWOCSf@K<0+dRl z+ctHdxUC3FH@WS)ib?zX#0BZL_4E0CE356m(yqp97_V_K`%dzPORz-adF_*iIG#7T zUeeZhiTc69(pBWQcbq*x-L`50zi&~sJ5@}` zWK1kzmka5(ZnS?~wf!39?Mv{hrgT+bplgMxdQnqmV)djiH3djtCNzN_9WgiEwylHT zck+ZDVNWGCrd}#ZtoEx?VEO^NHnp6(4pxdy>LYEjz2oJFrQ2Nea^4iZBzCDX)QY5o zo@Kuhi$>N;eSOjsn%A*sPNFljt8LfJZ(rvg+;|grp(gS0O4H+Oq$YE`Mnh#8lh@u+ znw@Slv2=uLyWqs5l$NNWmGh-%)?+i!D|l#}#u z9MZEF!-y~B_sy?1L?BvQ>Zgv{n6ug4RhztcQiHX*HI>GJtx6;fS0{n&q-QVMU-Q*k z_02EZfAiy&8aH2)-*MjFBhzgQ7xEXHWBz(IyLLzmD=k*fAT<^)NHR)g(m*YW(yGURdR*Ftvb*4ol6z`l`*3y&z)h-KJ_0GLv;aQ#FH3H_d#O z4pVLWRW+&8r~3A*MJBah&6v9VYzL0857V>SlCilbOQTx#1$I{J!1Mq7>U4pVpAs}- ziExB4SA4VZbb7n^&TMmfhj_MlW_EfuC)_Ir+1crriC1O|>4EGb;qmP9>@nFj*%xI` zlzZh(;?dbS+m}5hYYH#Hr}sf-Tl#k4VSKuOx^RYYiEx&1j&OnSHsLbi1H#9ItA*=? zFA28@-xYo&{4BdWTh6{Bm(I<|9hO^=J1VE#h<=SsOD`EYrbJYAe8 z=JHSEAI!ZfcV=#5_J-`aIWGJ6>?gAC&Auu7mF#D;AIiQn`?~BOvfmPd^y2KJ+3yS6 zgiYByv%k#VpZ;q4*7Q%(cc&jnKa{>P{g(`%$zGx$c{3XKincFjWXCBBrl=(~MNr4wKnV$%<;0nhJ z>%{AXnc_{ETg9(sF34;WFVDOrb9&~C%vqUrnR7CiWZss!Ec1cP$1~StZpfXVyCnDa z+~v|)(mB!v(k0T{q|2laNM}eNldhA#B;6u?SNf6kGwELGYAKM8kX|A_B>qKwQsSkI zBug{J2c)^u5=oO>>3C_Kv`N|~{Z2YvdRRVNzF2;X{4V(l`6KeDE$X}GdE&o8i zOTI_`jr>RaUB}ntJ+dng%FS|4K0;n1JuPeU@$x$P#j+>wlpYuF7Jnk%p5K_?mOm{Y z<#*>t^JnLa`SbH{&tIPZVE(H7HTf^(Z_ZzmUzIoW%W@yeeKL1#?xx%=x!ZDgLlMt*L7Vg8%>@8$0hUm}*oA@Mx%BJpG5)#9zwfdVVn5*gl*;vgl*yH5q2VfIAL4)xrFubhY=R=a|jFhLkWxc*@V4< zpGDXnzKyV1{7k~mIe!pgujHo@HpRYSU>kq!gg~{6E?{GgRl|qDZ+BxlL`BK!v2=9CkPwj9w+P!?yrOmbB__$$^9i^ zj}kVE`!iw3bB_=*@yMwTUxE~XCF!v+EW^q3x>=5n;gw5b?Cu}D7eZtzf z?-4eOyDeegP1tt`Tf%*tu%+Csge~K~Mc69tn}i+5eS@%Lxm%KVUr*kBEqV9VivT*fHFP2wTN{Fkx3xRelZk0m{$k-cR`j+!d5x$i0v9-*WGz{CC{tl>eT4 z59JSXmr?!)?%k9>#J!91KXR8+{wMC8lt0Y9gYrkXw^ROS?roGm%Dt8Hzi@A%{4ws$ zl>e1`6XlO{Z>0PQ?hTaxhP#CF2e{W$em{3HnQ&*_gc#D;4Y;6PVNHA zf5M$l`CZ&=DE}#U9_2sh&ZYc5ZXe~p;9lLDYT~(HoMjc^8K*&&)XNZ;fKC~MhG)B5Qf zaTjIM>6FPOk~M25eKTzbWe3HSwMLXRhlFuY1*D+t(>G6hl>PH%Bu(n0^rbZEnbQd4 zxtEZ%{HcVBFDCTN?SxJ{g=X_7Q`U46NgLTlXt|fr(H@$S+DaJr!xJg{!4}GH-%Qz+ zn<#tFM#?VVK-qiOQ}(_WQFcW)W$!~#)h7ul3uY*F@lld?+;%HE(;_9l(8 zOI6C=tWfrr6_mYoIc0BKM%mk!QudA|l)dvP%0AIS*{2p$_Webaed|cdzP^yMTaF-b zu3A88JEik!#y1L-eRCdVw;oQ}SLagp?ZYVh&Ky#H)}fTnrb*wOMcHj_lznd|WnY^? z*;fvs?9&HRcHMN!u0Dve&rGB28kw@sN|b$0r0m)}WuMPcc72wzn+3|gkfH2`G-Wro zQuf6b%5G|=>`T}Vp(9^zYEJP&j-LNBk8$!p!9e4l)`{8RbI^7nCqyhXlQ zzES?X{2BQZI9Yx`zFfXkeyjWj`E@v9o+H0p9+ijWU2=>w(8`5-wjr{yN;Y3Xm$U!*@tznAWp?!h_t4(WD0U+@j-E7BKn{=G)} zr1VkgO6k4QyKpXkqjZsUzVvG871AD@kNc$^l8+}4PLXp@l!aBeo%a$ z_-^s-;+w>a#S6q&i!T?;;()kQ^l@fAN!%j7NbC}i6&+E-`SeJ!ARa0nB1&RLY|8&5 z|2WRC59aUB-<`iJ|HJ%u^WVV9_=f!F@}J6oH2;D8d+?VNZ^~bUXCYpdKPx|+@6X5i zm*roa@6B(@pO9ajKPGSGmHbipBl3smXXU5o^ZC|1mwPhzSnf}`-{pRt`$g`~-0it< z=f0l%QttZPHMvjZKAgKE_wL-=a&O4JHg|6BmAO5+q1@@YF!$2jDY>n=4Y(^gK6iA^ z$Suz;&b8wnWoB+#E}Lu4{xkbG+=D!n{Y~~)*`H;9oc&(*TiLH>znJ}e_Ui1%vmeCG z$)(x1WM7}XAp7d<%W)4gklmT}voFb>l-+`xov!S$StqMymu8R57P5zC56Mc|Otwk* zhw!-Yi146rzi_v3m+(W}<9tK7S-3&?obV~(qqyODkMIuRO~OUOYlK(f=4V*w7h>UM z!i$AoVUuuzuv$1qumlCqdK@7fF3b|93wfbc;4)7_3E2-rfFZyTUEV>lrSvdL;gzB!yi(BFl+L2G zjnbKv&Y<)VN)M)VI;96uI*n49Qi)QL(mbU(O0$#-lx8SRQ`$;t3#E7j6MaZg+C(W& zDMu*x45k01^l3`}LFrSJK1u1{Dg7IzPf+?erGKULF-reJ>7$hXnbJoneVEceQTj(p zAENXRls-u5?0HHU=6N9nIAy_eEoQF;%hcT@UHN`FD=&nf*Gr9Y+g zE=qqw>7A6`LFtbv{Sl=2;KTp3-Y6{T!vArSuv~KSSx& zlzy7hPf_|wN4z!(5Tzfa^h!!UKDwuN8>Mfh^evRWnbJ2=`o{m)-kZQ_a#eNz_4Ho7PBN3( zl1X}c5+*~^N$pEQNG6k-WC+<4NZ3NHGtJC&58XW^0RlV?LfAnCL6Jq?mqk<-K{gja zL=aI?B8t2U2!a~|uZjZd|9kGOr>A?8@bXXbE+42NR5EqzId$vYd(S=Rcb2QJFHzSQ ztLux@^@ZyC0(E`9y56I%&r{dus_Rdv>vPog+3LEYu3dF~)D)rF}WmzPTqBo9%H?XgJVGu9JALN5E{a=Bdg%H=Y-Tq>7` z%jFWeJWMVZ%jF`u#Bzz`63Qizi!T>XF0Nc0x!7{C>TMo1wXgSbwpyfczftCX;2U-ra9B4Vva-iiv%YpxE9O&;KsqOy{8dD$q zzt)ZR#VrR~4zwI-InZ*Ta$T&~l*VK+A#uZ5&v+yK|Z`(m!laPpy05X&vL5 zl}C3cMkrfz=cS+7`^tadpQ*<_Z6?hZ7v}FeI&;y?6*teEKeJRA%v|JLVBLN9`D=f$_xklexclzM7o4>-=va<3YdN#FKWzsWIqpSnctIG)u5F)Z z#TQxD(`K%jTUwgAD7Md^xu!e>Y4`k@>kiLcZhT)D7N%v@ygA5u+y<{~#bf9869 zyqLOulboV+Gq2G!_{Hj-tLAv#;rU0ViwDaS&)7V)dP_YN@{s!3<#K!#Kk7Zv`+IzM z>f2UW1wmMbo)g%9VcULQS-$VZUS7t56<4kwrCycC92S^YMUYo^oCkgt2bmjLrzrb> z>gxvow0~L-v>a$T&~l*VK+A!a11$$y4zwI-InZ*Ta$T&~l*Vz>gXSPSpQz{r^8|W7~dD%Yl{y zEeBc-v>a$T&~l*VK+A!a11$$y4zwJoIUxT3!PfuZ&WDx*EeBc-v>a$T&~l*VK+A!a z11$$y4zwI-Iq;*+f&PA@KL77~#?<$Iw7<3ex|Rbi2U-ra9B4Vva-iiv%Yl{yEeBc- zv>a$T@Z-vX{{9hD>i-)Xeq1@wes{}(mIEyZS`M@vXgSbwpyfczftCX;2U-ra9Qg6z zfY|@$$ZL#=?@in}{>|~@W8WO>nr}9@ajX5)a-iiv%Yl{yEeBc-v>f<9&Vi=|!&@%h z(dq7rCHzWt`dbMpu0tAzts+`g`1*S;H*y*DS*2d=#$xpn#hAD$Pu1c$DMp(erb6zots~tzW(Y}4#-pM9y_);cQjqRYx;0` z*K~S(d0}ooUo4N7^ULR-^vbJw;O6Akn^#|W@&l{i-s{@Q4?HS8a=cVD?%SdUhOI&da`>#k)E^crTS&z)JN zG_$AMafY|-<%JjC_l4KZADNpk^)IhjC1BMrz2({iH{8-&UhChz?uYmD{`Kv9o@(nZ z?7VLmF6TJ)qveb z8ZNm1F7%pD48(ye)^wuREQ7FyvJAt8!&~;zjdSk58@(T%)Qj2wMK4bL>;XEl=7BxW z>0B_pW%usR^Y&_?A~UAft~%TM%lY+Nd*Ws7gM68q&@=MgJ#ljFbvI96d&?sqIlb?? zYj3*w#=YF1o)H1E{+HL^c;K47H{LdVb#mME-dk?I?!dLY=$ho(o6pxId+)zL(rX@> zPmh)}TCDV5T(_=e`H9ZaG2Onlcl%H3-R-@Xul|ShV*hnFCI_y(R`vMYncgF`?3unX z*`M5)T)Qv1X?nFmhFvwXBCkjunXo(U+k4Z#y;meZKnGSkzkY75@51Us_vyoG%l&lX z#)MHhuJ6jO8>=u|J?V>eO2FCja7;LV}nL_qEot=m3y8OM^$d; zwil;WR(W|8$3d0Fj^~7F>Di?hcySiHUY>ffAIEkUM!u6pzCXBsgQAm$c?yNf%1S#A zy{xhdrz$-Jyi*j8Re6~oJCz-WS!R_{VdcK>^ZU^Dqs)p^w}1bHq9b?8!Y}|CFK=RYg(SL6rwq8aSaB z1zd~BO})}ibBjAp>WAF70>ALRkWOWlYnMD9@TRPP|G1)KJ6RBUt{+EP4kb5_ODpoD zs;r_g&9Xd9Z8uDdw4f`QWo1QR7k*WRMUh9IUpW2A#X`pnecK5#*GgTt%EQcY_~y`x zL&t`%wi8=k892Tl2euu!PMUjhoCc-qM?vA|RgoqaDKb%Bm0=#ZR^)kQSyn+>*hN_| zC_!jhewo7aG>@V(wHO2^@?BU{WO?S;fem|-Sdp<@Cx*w46}o&_RD^DsMNU>Yb`b=2 zY1@@uMkT{-1wjz`IekpyB7~@ur>W;Aks`zVuu?mYir9^dGIsnp^L#&rCsCGqRRkq3 za>KaH{HQ2+j~%5}>02-!Ze~^zDl(p%*=ZJKrC;#__-JQY8brAjrLnLZLO~k&g=K|> zpGGzxLVF1_ZQD*GFA4NVRUyyhvG3)18F`R_7ghxAB2M!x3N(KLzbb+#%gVAyEj!Ai zvdHOGUM9XG1FQ1Vi>)+u+$!=)i{CM7QRe4mLH}XBTNDLLz$smbFz~Q8VqBSXZs{kU zBIAdV>)9@!5=U`Jqg|Gc#{flXo`!Lpmo{ITXSrWMH_shb8k4WGEH7koWr?fE#8q55 zS!h>&mHBp+*@YKYUg4ybAH>kg3SqBn#W4f!!I#+c%P7ipTp=D-i(Q0TIhnKORO%*?{9;`K$D*osVLGtiOm*%gyCEUJvZ z@PTE;lyjpfD1)$!>1mJ#3<5lte!4*vxIt+#D2b)W*m<62WeWQ;7Ks~Isa1t#8M170 z$4Y%09@Cv7^sORv+$fJ_Jvc$g;>=hx$%Tqc$zt>ZL`mqSE&~{ZFxUx$Dy)jE46F#^ zP8A29XIo4hJ95f63(DMcm`Y`qCl@F(Zoq^Kd`2rMGdn2kJYbwG=1t&+h$o+p!sfs) zs|>jTUxM6aK01}>T7m5+vx*E4#Qdx%5K;)$iiM06MXWII^Uz_ef*^KWtKwe_Ds!kp z0;SAUm-!Jm$@z*59Q55V&qBl{V$aTf=9ZJPmYsl+W)vCPuteZNxhSF{4Sc6SGzOOE zrGA{8r#~vJT>j_7>6|Y4f$K3MmTQ#=cRt8rg@%EbhIUxRR>q74>``QxcLlOF zgUz{1KYfQO>Scu+$y#H){J^mp5^k5a6PAw4&fyoHr-W{pCFd$KNMV*@X|sF~eK66> z!aNW87*=!`k%Df@GV;%hj>Z03lwrX3=^=n!?xY2x&dON(9veherDerxE})ouh0W5fJSf5;&rW9a zM)puBE#DKd73X1)TkP*FqBQhbAfBJbaGE_ZKsNU!XQ}R>LR9P}vi}rJ&eEoL zVaZl+TeW^;75u_t2+N8&Agh3B$ZnUiOITrYrXrK&aqNl&VYl)sUW9lE89te6R++MI zRZ+xJW!I+D5i*t`^yzg7hl4CirWF~sk&+3{27u%$Ji3dFsC>(-SbYvnVyYy_+CGab^3 z3066*9uyI;NKV&en7~=?Ajy~~KKll$6q6P;CuM+msl~+MyHFxvJ9+{V5gBIXj&Rh@ zlI@BNtAlNU4Z`um5*Y~>BAew>proSgv7I;`Tdgn^@s_c1p>AY!1VuA&sv_B@$i!^S z@}cY~W##xK%bkskr9&5(3+N-hXN9)tdO?5$5hYBflp9%P8Ab8_twN@Qrkk_Z^3{=D zA%hVdL55o3WUgpK470=FMhI{;E*HrY@PA(BBOpUR^v9DesynDVXe~vFhQO3%>p{2_ z?0hM*kj2MxMbSg9r6?61T4j{Q9up~a(lGNqCrdVKGAKP6{Inb&^@wdgs@VD}nLMnY z5berGm4T%!JG2F)h3AHWom+8aTa}+Br)lQo5n~yJIT~bK6}BHEIT?#8LR3MOx1f9oT|vM3Ik*eR!5v+dDsZGyz=4*(d^h&fkMH2aXD8>Ql!KCCr?96p7Cri9CBhVMD( z8ij3R4j`hv*vok<^O~uHu8rCoWEcuTj*-RUMeSkLTZyU3&~ckBia|t0ioGHY(e)#@ zWXH1UQO*vX zQKgtKp;ve;S|{=tC?){bTa^qeGO|IUakC0VNzW>#SB4fC(=mqGmbFr*h(+c}DH0Gd z7lrI*2*jM_WhZ^QH%M3nEZYui0v!%U7pd!@IIx?b&_~#A9_n-)(>-=7n3QK|G65P7 zGS*Kzij2nsM*Tt457~(-H_fnygV?L8ppd?C+eS(27Yef=7EX+kM%#ZKkEBt z-!Jy%eP?v;?YyLOWydhy-`M^)jAT)LKYItpMH-?D!T*#M&gd~$yxh(r{)I4SAMgyOIN zhmdUr!j;`94$$t{BLOh%P?^@k8K#U|plfGrFE-+v*Qh1yr^!)8hF3A^_!3l0*e04z z#Xe#o7+DDwLQg;!L^_HL(=)2XB*Lmjgfs8bDmkLapmt!!vvDyl80W6<@f|E;Y+S4! zF_6&QSRXcfiW_EZ;<;$dSinUF#^EN16&W-+_IVZu3O5Ex8liN6tDw2C=8)4CYoARr zWG||O#hAjZM08M8Cg7Ved6FUnXUmLL2OkhdqKP;byA2=_(uHA@*?_8trXzZTRWc;# z10|*mtFnxoWKNOs3uG6YksDS3L4ZD1%vR(gbVGLBz{CEk_!(O`TSRGLq#%ruohWUX z2bh$H^hZ^ECvwI@fO-W}6r+%lWD^mq*#YLjhyhE=n7l}xBFrPu3QQ7q1<^#4gNh80 zLJk_iu2Dh(HOYd%83GKm1#KRbgQbB4M7CiyVV@31BD1eyOtEReV=Xx`;<)I)DTCU&ZDY$V-s7 z*g#-jB|B-6WQq)0V`PhdXa%C$;WRqL*aJ6BQYN6ZAVErpp38}a)yuLVjILx zvDom@L1js0u7hb@p;fVh!0j;YfJ|7t^cw`gOP-*}fOTf5?lazL@>a_c1CkssZajXCem-k)^>r z1SMjvqgt_)02pjLjx#rTobC9Zw#s$drBnd(k}TBia@)F@}4I7{ds`CS-U;D`EDdRBf|%EzQw^m=r)_3U?N6%Vc5~VCgvfZxJ#bfN*e{0OphcQV#0~ zAx{dK~ zXV_wxkQD|wb}rT@rm*0DEZXE&MaJ_A8Md2=iHKeC9WkAH(UP1H3 zc*Jr|Zc=2hQ5}Ri1}d6ljA0*w(fa_Kj8DmwV--|v8-TT#@M1DKD6N>)%t^Kpt4MBC zWI*HL6&5vma?bXg3RD#_-i60IV}8$QA^4fbVuGM$BFmA1fCyL+mYdw5$ONe1fW_!T zY-(a{$2^DhcfdM>)Whg!1z`FWm|beYfp>z|v6(Wdux66$6&WNRLdA(duvj8Mw2>v+ z4Jr&@$Umq6=(0eU6(0pERmjeY)J97|;YOxkr+LFN;Tv7{43;=50b&9~h9O5tB49x^ z3btn`Bg=eLS>~jJqX4zlu~^H=wTcYDj{_jhB=)gB1@1uKk9Zlv9-+v{K!Pn| z#DG;GF3@5Dk(h!7vrcS1EMw+5QUSM?%|0FR3Q=*G0~V7bIiScOdH`Y^*1UM6Jd`{j zGm&BdMQC(T=4s@E%k*UfwXnj0M@4-qK%{Ipxk`~i3#9{$X$X|aYXUyR#vy@pMWSHC zpj9I{JygEZLXXA9#^AKkco6$8!uU$f8z5v9()0GiB67F?J*Y)y<3>MveGuwNfU%5`%FM`k2=IqI$j z=n0y^HWeVYlcXl&!3|!4pkfhwA$nh6qn06;T_hfY6Z;=ogtTMs2#f>7g((Z<3}$HU zzf8z<=nQzIW7Cb0oz3?hbVEQnCZqsj^bMJe?SML#AygSOU`Wsaq@e}6ZVx0^sP0ts z7NLNaR0yPjw8cpUHXg!IKLxsp1su#}k!1+$5}OE35{HT>2x$-``xF_RipU4lV33D^ z2iSsesRDq23M!`vx|#1GVt{a6-@$}uP;l8~xD4>TImzXU4Erh@QX%_CR`9iDAwC0i z53$pkDIO|1YYv@80HKJz8z?YmJlV*=)x!OI6>~r$W%ZZh+QZ18$H;Zihad;C0{H`g z$i--88lWYBv7%;1fe1`i5BSQ!{)a0%^c3L%pduQ%i}wi`z_#T@F`WT_bAawZS>PJ_ z1pQxPTN<)oC^#uB4x0Fu+XU-JpX2;aG}d0wMO1XxwCHWU?3I z$-{(>iKxS1V$X=dPXHb8)_8P*F$b1I8kTH^=-4>BF@j4@NabT;X8({&Nus>IrMh}^PMyLqS2=h zy>aMqLz@TxuzzCk`Gflg9-!Lb{(S2{#B8| zJHx&Pd@FVq)0hr09}z$F0XRU!jVHcIbh{Kl0nH3tUQ`sB%S?LX>-wWg0Wl?xfX-H; zldv=3Y?CFyP9#__DlP;U(z3i{U755Ku2XwVL%B$!ZunU zL|HZkrgRAvfezVlUHovM4-S?Yng?4_Z2YArga3*ZjE^03nZ*=>h+}^^0U{w|^T(or z9oQf-Ryi0Nzh$Yhsl|XcU?s+1)V)EE5=X;*&zRL3u%A#>Pb70P;oKtI)7S zaYkUW_Ry$6H*mTbf3C=2H-KP^D^UDCfa3@yaipMgv5eW8SXF!q-->xFwj-DtKrhHE zMl6mzwLU|)0? z{W{OZZgcTvc^JF6D9}_`nygVgX6y~bY_Qd!r{LQLB;>Vt+#}=PbZ<)ZM3fKI5xh%4 zTP$m=dv@-iViPBHD8+ifHbjX7=mSu~C+cLV6L{hMFgISW1`1uMk!Esbk4htq?1=s( z^m&IRi=vJxfCA{D002V)ToV?B+K57nFpY4DqaXk}39KoIfY=QvhQ^0=XBsM^SL@Ei zgi~N%u+_yGrVJAZ_Z&tKmK{4=NH_)<6si{PC_q4_4Ksi-0_9{mS+4P0nhe%35Q`XJ zxcUmw3UN8BJs#|joty9+RyKyH2rOo(M;i_>>~f}i<{H1Pzp0^vc$e;sjq)s$oG2Sa zHPZ$i8Z#C97iR=BgUyw7z|<$yCKOjDn{o#3fa)UQBF4K^XHbAKbS)$>ZdxClEo4bJ zECb9Qphi{(Xe;`Cfi=Kf7xV^n#>2Gd%Q2Mi)>ImXqgXYSW}wUC6=M`uf!>2cgrbYq zOPB^eDn=Cp3>bpl2}PTQ;)#olt%Im3yx}ZCut}Df@jl&|hF0eFnhGP-#5J&3Q)w0_ z@sOHIv!IRLHI-)Q&uw~;Ffq_Hus9IRfO^<1#9g7d0sx|Y$Lwg>)tJZVtT>7Q1$_)N zjALL-oGXR#JDLo7u}E*?Ot8d|tQgVw0t%oBuy1j%3GhOIglysXGC(dd>tQE}Ng zjGxqNi4BAu9#{nRSX3K{4ntu>wZd{G77#cH#DWb3SQ{J6!ry{!k5^LA*~0F06l=gc zBp?BW=D~%}1)54jB;XoN1yhe8kAMJBG!Aw?)PHbFX1VD4z=j?`h3p^fOsGlNJea`Z zcqghb!)Lp!7z2(g2%Uh(XfhxQ2pG1vOdQdO6Sh_Ym0~bgv^5C~!<(8`U>tb=#M@mO zFRIC4fDut9&P}!8O1v9rmcU>xf)T(PO9(OSgA9QLV(PQO1D~Mii9J&p@6q4X(6w+- zcLux)vk&JzJ3ht^F%(P))Hu*Zz#WO`0?kD;Ml=E+u#3?*aA;svP?O9vUadRR5cYmz z-5DS3BW2%bSnvcBj)j4aO@`Qx5%9#};1x$n^no>fiF$zN;5R<|8R~mxJge?ZGw}R& z-5D$lFU*PTVL>sA@l+Dr1^7!a9UC6;MHnamM*>E<5}8OeDiK2fjv$9%aK^K1GWeWS zgf1>fFnR*AAcxN%OA5${twgkI9GNJD0;q|h#U^Y6+Tx)=MSFoJL*y;q1HKl|OC>55 zfoc)^Gu=ZKBLo>e8oPk#Qkj1NaT_t(1;HCASSaeoTQzGM20-o9oxv)@=E8G|pA_4g zCC(fHa|eSLYzX@g|G$gNKE{A%{a~wr2TF(;uu7I0zfhAQ6a-TMY$`&X%aJ&EuE0sa z1OWF4*RfzRhB~1N0N;EKa{||ShKVFzOXD~7H#H1Cj&*08N?cx;Cn~a2EOp=uWu#() zmIVB!xOoUX1OP=d7Ys~G7tAXeDJeFZyPFy&iuX+0P5TD zpkx3wA=nq_BdR=eo=_z$Rd4~|N4)i*_?Wz~1LqeGaG($|O)wX6S4YOTG#N3%nJEhA z$3z1v#-Gn7gMi5k%r`J=AU1UWNcks$@{oN1+aPQ3J~O_ohP|o7E>)euW=9bO<`OSR zNX&r;Mg~w$55U6E8F64>&;nR{EF@4qq*2ITmjm4q6=fM;9MHXK7$19{>WvFZDRw@% zgg8dfzc95hnVC@psp4n>gOz;;A00L|qL~<3fsMhqf!~7I_$&QUSoeUPKzNwI4~R44bGkP`8}JHQMXU@#*@y!?`N$6<9hn*IxDqXn zN6;f223r-3fH*KrA;NgDE(7DU^>{b)*F8pa821{^TtH0lVeuxgEb#$?{1qkc0}r62R0`N5ehOTnaFUpd7(g5xA19!Q z6RZz$OR$9k>Jwdu{cF5TlL3X6phQ#>e!YJETVpHLs0UZ&TP@+y!b{w=5vPLqzAY$$FeLjXh))?w3 zas-?LKQD2R#@7@X_FhrWn0bgR@frXz0%jw7*j50Xfgy8DLzEe1>k-xhSYiWlNHh>~ z-uN5MVMH0AAW{4*P!d##24^qJm>!ly`-mEe_Z_H$NJLy-pj1S6vDdRZBM;HpUTnV8 zcyS;8T!O>!h~))3BcU|l)u3`1sCW->HIw3jXOupmVGxQAs7}fZ-Vcc|z!PK}-&VcB zpWrd`O0ohF4&ak$shDtt4x+b{T7ohr0+X;e0+t9&A*%;B17N-=b3jIh<}lbHZhM(X zh!lwu1+_+*AhecgM5w2v8-Nd>h$7&HAjSk*Dv>_~@&r+5qUJDy2+syPCq8y)0@2{K z60b%IqAflW7Cu-j6CZ~gKtBDEm?t7+K|qSmkeW4MR`iz`B48eTII^+`?kBwjl%ar) zL}ig~BH^N9m*cd+0n1vV2|3Mvx}>NyP4}uli5PKqZ@d^b`Um1MB20!w4X96WE*cwN zU!Y5e5KC+@iRS=-#08rZ!H1lTjQ`XNp`pL|CQYSbI>`lk0)v$jx+-~Ze1IU(ght}e zK)7%d3xKFLyv7pCTH*2n!xK+ENp_f!#2#42ujWDubI(|Au&=yNR$HxHIA@3 z0WGljB(DIoO`;zNR&dZz2+BbDtcbHAR1cS$WO3Z78N)8Zq{qQZLJ(`7bx$G)!E3l$ zL!7h(-yrY4NAH*bZp!x$%~|GtJVV&e2pFy59F)f#=C=8WOI%(lF)H z(Nr2nWZkN%G|aFK^++S>h#6#}OI$cXK$uk$lTTDM7`2T-f;~oP1-Ka20zp-T$KwDc zas;nt7F5PhX)>%8g2@42Feb!$0-2Nq+8ocNgpI`@--0OM-XhBv2#@Hc5{Dv64Vp`l z8-Jq7;5;P6fVhvG} zjRJ=c>G=Pb8=bk%PUFv~-fz78L>++1sfmwIbjkkz=J;F3Q}O^lJ@)*uy`%p=`p(h0 z(Vga>n=dyHn4OXLk34x~&+r$AUp;)ousQVMp}U6c!M`1R!{BX$rwn{zpgR!vf3yFs z{b~QUzEAf(zi)5n-#hR8KGy&KJ9K;mAwlFAvw|_`3E5)nAybb~9Z44@*a$t97zVUP z{wlG*63Q(aH#<0C4Uyfq|G1)qECcqzl0kANJ@F;|m$%`*be2|1B5L!h71xR#}bah@TNVY6F zvRSY}Q=-MltC2ibQsfA=0X4u#!^i~(!1`whCHhf@Om-C3ah`N_Vk&NDQoDg2Bt=dV z%_V6bq(G(tnx0tWnEqs;l28YvD`pR_Pr@ru(6CnNkIqabAg+-KpLBI{Rj869Pqgg!qoc6N)W~^9cw7{nSB7qK+_pAkq9AvkgUssfL}7qK;rqx;i~o5>g4t zR|zHd4unT`2DcpYiXl8C_SCzzIR#PEm!T=_1Qb!%cE%6!NW18fDb z1cQ($AwpP43X}XWg2GeKbOFDaYlIn)))?aGA%}(7gE;A~Ca5;jyppcYP$l{hUlC4Z zRz1Um<|D~UB-4?(O^hxM0+1mzc#>;y>w}hO1>u^&`as5#D42A0iYk+eP;YXSBn?7R zjtQ$LAQW#if|PYfA|5Gp{I1L`@&B+oWpk8h7^x~yC0(7PN*q-qS5DGZNvb9&bpSTR zrx3^l^e1K^*#dYD$N}YJ*+>aBCO1_QpCkl~fg&Ubza!(0Low;$DZ~)MyDlbByDguhoe?*_i z9zv^#!628O3}dp4*;+_MU|T1lMgoh7olLqqPnE#fMxKJCTPLbERDY7L&Q!G8jjWllWftI|L5#EJPUzJ}TL};B3U(;g6A6La9JP3^xi!fZG8hj4)6{8ZJAF`J+k+ znK+CaVSq_jl>joEx{;HvDz#){A~kX~C0(5p%%0oGLz#4SQZS2%yb70?R>?ghDMUg! zW$Qv{qbVb?86Cofi8_%gEfTiI;KEc2VgU6re{@zb=q=fKEYy&7FTaN_dL?2Ke3A$^ zsd9prfOU+ozy>j8WLe4NMG)gl)Me7uX~CF_Iqp&kG-EWWMTG#s9OaFY#DnLOe+V{| z@H*rjP7DEZaXaDl#*=ENNq0!iXJU2n)`Bw&`YVD|!ng&B z6l5M?3qLs;Oo`kWRCEZ>vjITa({LVwJ`*^EWsKjQ`UeO#1TLYp#PLh)JuXy6MM6Lb z5GJrn>MkYS0Y!(n1tQYXuSwk|`6fbvCgV^-@&w`~f&eXn_*scsC34h}j209N?N@Y&B5&m2PP#g8T1rMp)+PER0Aqj&MZ_GRl}f${<49~Dp;LrMAWi%1+`^|qx*;Jk5})qDD)vO27L<)5c3NC=ls%#61X%E;h$5AM z;WA!^{w}tFCQ$^*GZh(Py-8OfLxp{Yj4R?S5rd%T{)CmtQg4s;&x5Uj;8;FUFe`R#! z2IB(b)t%0}JM#n9)a>L}CtolA{fSRaJa_Dd@$ZcN<@m3TA2D9t|Hl5ubso}xTHhb^ z-7_}YclpTZ@JENAF&qtjtN(9nkN#hc{`T0<41RX-<%5qH92oeGfjfv8SROy8^WD)) z&3`lBHDH;Ci81)Akymxz%}f7Re-ymYl-GADtVPnVh^avA6N-ilN(2Z~5HUtN4vu&R zo#{tN4~}QT5Sf3Nr-Vr1rk5B++xQnnMpOYJFsO?q8C$60R6Zdi8Pot#%LdPWgaSjB zAiDyn0x2$pBoNhxcL}V55Y%sI=RMKHji8M?RBxK@TgE+#43RKoJy17AQlqi2$s}eA z6$1+mnczT@yu<-T2&y=WQNbj<7UPNnaD@4j3Gnx7pqi$M9nizwFiG(SJx-*@69UC> zpss_~0;&-&2fix>p@4P3%#b?-D-l#faVoYyyj93pP$HhMLgU>vnPv)x@qA5&Y8{x7 zAbL4MPdw&WTL2Xpa-^>zFi6;@U}Hsn zQ&YMAG}W1=hL-UP-I<2@YM1NIpt@1{k;-=B)+DYXBH@A*PllV_1tpX{or*X<8!`cA zl2R%zYg}rm=|tM4#yfOp8kXERsyjotEheUDNs?+x<~H68$>?QnWl{u>ZH?`XuoMcc zu|FXi$!(F65t4OeRmR5@8O9!iCm^E+Fa&Iy{~|jJj9Y?Q*-zQ{P{)|bpaf(@qh$k+ z;4P0@G#ibV>&`SRDp6=E4J%r0uBkLDy*R6>G%V42v1YF% zM7p>s5L1ABARFivX!86Ot&EThbWjR{f|FyDV0Xom6-A{;%FvS9HB-I<1%qVaQ@GY#u=nVO0O>O7e8TzcZ> zl((b)57C~IZbLCbF_0ws84*i`N48k32ev)5FU)t0cmyM*k9a4^QhwPvMUw&4MTIBA zSn_)D@}cVUkQ6DB*c@zV)FD#nBtHk>SIT}e@0r#_F_CcObtcufG|le4M0W?1tV4LUmVDxlL;;>4~+`A1lj zBPBsxM7t1Qq(TcYg$X_-C6m}A3PZ5sNUCOqVK?H3F#b#R24EQ=q{JgmAS#iI&?1lk zJcJBgs%(&bfz1UNhxeUYRyaqAm?PQ%+ZB|7*BSqz$N=|J=?Hrq9GB>&fDlX-!A?%A zl;jRbutU?pHpSnU6LChAT1nnGfsc6M>4EV!O{Qs3+B?-iHLZy$mCOmpX_(Bdhr4;W zo}POROH$3%RGJlRxJr!@Wz9e&fI*48puixAJ{7ejBNt@@I7ZNOiW^`fk=sl9CWxJ6 z@Z$SpsZnddGX74J0TIS*rEnOgF8?DmnH*8jI(7^R{F8dBlqDhDkQ@i)TJh!XBs-HjMu5oG_4l$$eK#CVuZum|9_7$^}eZNQ|C^8Ve(az z*H4a2d}!j%32Xdo;1;Hw6&9~>F@(7>GoR{z(?9k{iBL*K{yp4At1zR`J0=ZT%I z#vd8?+}HR|x=+;TN<(ug>FSbJO=H`WuC8fCD08Ds`J}6hT2bPqQILGn)m5zs)FMP5 zlR*Gj0`tKd$bi7IB`y_Zi>ztfBjB?DdTeaCMx^8?kxJ107+NZfNEL37qttv>6-F8;)Ja!W>@rDy zYZSYmbainn{OF*SAyatX=j5TV^<3A>BLp;FvXb22MaR4T9ajBqEQ@{}T zHRC)iNV>YfRbEivjD%O>iR4rYR6L3RNgO!A zfaIG}G=yjgs!n22lU#z!nmlldD-%K_DK6sv)fKKvz$7wv2yxFT^~A43!sjUrf&-ga zMSQ^Iy%WlTt}1R5LI#LH052Bv2M=_{I@Kkvnp#pxSJ$}0-;9L7jR9m$jT8_FkP5oN zxJs2EDI_V893v{C9e8K3IPr&(5kRQ7xJ|%e`3cn}$+Ey@ zgovqoe+N=eZ{U&rH1heNNq;qsew*$ zMv#C?%^`{~b#y1Ggg)iN8^gGjo%OqG;NR|Rmz zM@T_F;&Tj$LW*(|KRVIpb zqp}-GSJlljDY4loYc}bsdRHbvdUAjU)-&bH@ZeD$f+S`v0_rs5Hev3|IU+cuFlVU{ z>`|o@(3IFKLTfO0aYyTVS5g;}(n?Yw6nonukrlTD#k{157H%D)yF}+Fc7qfdLbGsV zAf|{BA&Y@^UwTRRV#Q~qaHFJ=%Ke(-gu>y1t?#M^7~=V~}(`MTcr$ zjhsVC*Hv@~lx&pYlyn_Mhq8Z-O35T$TYpzWJS6E_iVlwSMh=Xmd!eEuMHQ%x2+&LD zjD!=i(5YU}XMFfik_~5u8 zB2yB*B^VDhi~3dgbfx4io-2@G$p=SwMU0SS9WW1P6&*}}wtif-Xk#3W#aSb&8X)Qi z5*F1_N}SV0vTlI{a|+{0iW&!AP~kuo)=0YND>_Y`AxZZH3nZW^rorz z4{asKe|G40q5$47`IzCSO>P|AH}R?dv5DtSTsHol!B3CBdwhO;_t+PQy|JGgyLtE{ zW0QR!9sT&|%IL!e-edlY`SyW%bIzRU|H8ocMm|5VyZ^=!bLbyOUO94o|Euq7@wazc z4*YO9pkO&7C<)yoAda$a6m}qvnGhI4!31`cBIZ;>)qR&a~LV{yD zr)o0flMp`$N{JjIGP#mMD<$O}Df2~gJwaipf^3aM)S=g7Ba#b=bBnTN1cDOH-_f2G z07=RmBABol5pPl_m1q$nt%=^J?gpj@#ugDf0?ZR?BIFT!_<-?Q5Qi3brc`f;g(CKX zSTKN3j3cVb6Uac25853eX(2id<`TF+m1Brc1^p+xl`vW=N&@jyPN_4d$xw6=F+l#c zn4J{c!2^sGB?f?4AqpZ$AsLBBLUF>vLLL*E01*riwp@;d>zJAh4i?}6PTRoSLCrYI z$-y`ZuHg`r;&~vlD5I1OLW#vVqRt#ZKY3D8yH=uxJA;}GX{S{0hC^t~I1Lft7>F)T zb96mQxDkd!z*J7W1QsCi=M;}8U5}D`RQgsv#_uUI% z5C($VDby|nnI&|e%`Y~-s>n2jAB;cJy=jO(7=NgH)6m#AeqWJkYU>-H)V*nF>KnhO z$TYRAjo(#dni`JAM>Ls+c9HQLnoL8p$oTb|H_faNop;1&SO&w6h0yfcRKVrP+Z=m1b|l3{m5ax-$SmxW*s!QL1O_&NQ^& zjkhZ@O|$8YSE_F!Fp*%z2Y)=`shUc|BQYMKsWd#CZK0;pEK%bbioH#fIgOtc`+s`) zd^!J5SND7E@CPOzJ-K1x6B8>F7mxqT_}fQ1w$C7KQv>a$T&~l*VK+A#u_c)-pT#lk9dKhR6PcSu)$@C@Z zPYx_45|OZY%4ZT1D`(|NT?I(i=~rY(LZjR=kzQo46Scu{f|B@3d?i01 z2v|5mci0I-!}ePlcCx$c_HNg5EUdY74js(&N#lCl6Ul! zFQ7V#9Pdhf6mU8@Fazf;@%K`-&vlIdS$`BIYsn-jDd0|EFkWgxV9-$j)5M!dyf2Xz z#P)(y${|+3cD#`+UCB*^j%|EJ=|-TpB-Tp>S^)!bRs8=vjQ7c!O2hLXAE&7_OksVx zrqb|$!$;Lrh@z3)D+16-bf!)UZY)Af@cqbfU(7qgKsd~ba2hgms4qap7NABsNuFsE zhSN2bW@kU%qNy}I>+=bE!Zh^C8NaN_Gz^+BKBUPsjEyrsqsTPPEH}D}!%Zt!9oJMy z^&^FWECK9X{2gTPJDf{NK_wED5mV$ZQ`!`d36TdB&E^Oy(m+X%B}mOC-YPm%e^SF@ zIL6OwG7W1E8E??v)bRMe{WX3FxSoAfTOdK4%Bq}RKsX{%26)am&4I#8 z$Z`@#@hVbditr|~&Tyx37>AS@lsGSrf$)9ZnTAn1#(O3H-|2kg|497*|9cwUk6(|H z6$h+w9~B8h*La3rOaxI*gI+D*Eul+y@x#~KCgBwbz0sp(OqNmmzhqLfObvq+P!uI5BYP@}_0 zlddl3#Mx1eP9sgas_wDbGzmEA>bg)(k1bBRs(!NBw02U`RrQn2ra)EFRrQliITx!5 zQk8TURoraT)1{K`lNFt&QD{l`n2rZ-SX?UUF6ek*%CB%BIAMo5X-+=Db2huPN|yex^NLQ>^Qx2XQALM3nvG6VPP#`F9g4?s zJ`5EM$cG_t91M&EF;0snor{Dt@*^msN#P5nAebl{6epnoUeZ0R=rpZ& zm2{t^=rql2Pr7r84g=IE{4nVr(*0?8fKk#tsOYGm&jxXfNw-pTnx2G|bW24C;Jgu} zoOBD_pN7Z4CEZ-nX<9)m>1K+K)$kzqq?;-_1P(VkQBM5-XAR$_{r|I*e?#@Z#|+;! zxrzFJ&zsmg{v8$Zf37~)Z*0Txhem&IWO(%1qnDWfYQCLV|6L=WA9=;dwLgA#bo<3E z2U-ra9B4Vva^OL6K;idIQMAseBGVMK>I`c#4KbNcpC;3|_+L$?nZ|GYgC^6~5Ip&a z6XUg-GYv}#8J}04thN*(1ztIKg>-OAvy-4!RU?`))Hk8lEAcKN2OHa1jFiv^M+3^F zeh5i#l=h=`KE+e1G()lJf|4mxMw9bNsNxY>qne7Gb55EvRr@GQPCN{i#N`}-h;lPT z4e@VWzExaMN`~Ma(w|dKKc;2?<@iWg^PER(DpW-$OPYi3C|*NN6iOvfNuJ6hz^fHC z45?904O$A)0CP*td^*M1taJu^fEWS4Pf@{!BAboGVlZ0rw<)c{@e3S;l5#=|M>5MP zOB{hBdD&Ee#cNB+7Ea6sPA7bb^u^NERB&fXObVqmN{XvXd=#gjQih*HQaFqWq=+vD zQ0IhQ$^%ky@1MLsb;cD6eFJ~!nOg%ZZ z#2hh7xz6mQG$Zqno|6pAAy*t!Ax;zfGEKz=NFvdQ$Tkl4Az70TQ`)1cIMhex2yJzg4rjt}0v>6f_*XdG0OoMw2X%5> znR}F_l={`I1ES#McvI+b@R(*y!xMsxpHZE`{|=rMa3TRc~k@U$O zCITfy$@iu9h$koe5D^3us3ZU?mrd1FnjJ>)RP{+s59ctRBmV#AbslC+{q@wRrrtC4 zhN%}%-8EHC-7pnS?V6gJ{4SLMJ~{cW$)BHm;p82YkDoj+dBNnw#8)RiI`OuNmrOim z;t3P`CmuR6NuI!G$3HOs#_{KnD{$TTdE=YLzCHH2u@8;CdF^ONSg%-5KAlSgoc`A~Di$iH$zz(+>jI`UH^%Okgr z#3MU~|AQKX?-+jh@bTf>h95qBHfIceW9So{Iq69sDpS4t58h zFt~5f8a#7wc;KG~{&e6s2i`RByn!PFw+zGsXATVZ|3m+u^uLdqgwO7OLjS}1&*=Mh z-@SeB?0Z??T;COa)1B{jKGS(e=lPuycKkqp9wJA-^j&Ql_b!!3%6xflVSe`LLNQw% zNtX^CnLBuB`7-Cu3y#en+$m?f^zAi`KVAReq4Y?#Ja@E|XU>*$Tl%gvjnCfyGpnTV z=LxAlJNdE2VsRlqtV!Xk6HVbzI~?g7biV7#{{2WeII@{dEZ~Je`v8h zlHR#W2p^gd`uzH5=7k0M!lL@VbMTM#9Wagm@c_@%w9dl))pv<${N+iHEmY~;5&5_o z{95v^zq-E%Y9iD4toklBjW4W!=2*E1I=n1YcHxccOHAX7_xnUm=1g2oeHWX?m)1X| zK3#}R<3f_x{M(84#dN+{I6AxVlyXtM2Ir8x=HKu4iTX9CKiLCv_(Q;W2+BTdV@)ooAi=)g9{g&o!vY*<=O7uBbo0=(U~)9jqGbjQNN(jlR_4WwE=YpDM5Y__nxyr4#DC*U)Sr!&xN zN{<~|TuAdnCr0Z`aAlcf-#vNsdy^2nxO0g)wRB{zC>Nz4%lgN4`>Ei-eY;HKKlB5c zeEwo;3t0Ps&>8824?i-+nrUM?0Z8t{+(QAwqqIYNDOQoVZWpnjV}%rO-}n-m$b$ zEzjx)?>IEKw9I^D6waakXXk2j!vj&Md$EP`o-#8!iprt6<@`u`OpPM-I%O35dJWaw z{G1-(t(4&G*yd8NWqx7tXnI8bh@zTsbh+2Era#-MliBgjie|%y-$evQBOKKP%ay`v#IiFtNTr3>Vj+C;O)Ze-5I-W!F{LuqF zCtN@CS{`EVy%Di>cy9jS>`hnAF7atcY9iaOq1`o&*Yw)gNYEXRq>X<5{7G%SpI)th z`r2OeO`5WrN{`S#ecefI^-niZk+E;LX}o^5S$?#I`icyk@rKpA_2Z{sNdw~Z7gihU zkC0!Twx32;!hbu_D7UvJ+`iB>ezte}SUO*-?Pco~y5+z3T5h_ke$Uo@y5&v1mgPB_ zdQdz4a$4xYo7c7U7_pZ|c=(oHoI~-e!_@fTy0j2pfH~>;%%#U zdyJsco^<2qdJXEM>c%rEpC?28>R#i~^v-W1G?+Z_A ztADzQ>TtrM7p*oc7E$X>SoGr6yS+D2yiMNpl3qjo74oA^jwbc8)iyP@Hh1~3SM~1p z_@>@t>G!<6*SN3uo(uJRezMna#hNd=fHuD9r%r0?ebFq-Dmy zTy8&0_wskwHq_nNex`2wM6XRgt!~@AgSPWb<0Gqm%nnK)H=m*J|6ctY^&W)$=F|24 zPuBMpm$-lWcJ)nLHHT-5xvHAWj~`jSOLm4W+i1KH9Yq-%v*n$v??M`OZiN~u`tPqF znDjW>P`!Jfy@f}vG>uQ)|0DHhoVA&!P`>|Q{Zo2`<$JcB#sju#{PvpftbcpzCYsPg zKCGH%mEE&-Blnn`Z$B}SdQYFKpMI}?+7;p(PT}5JrtywNj$l=SW8((yoMRgARCns1 zZkggHQ|w)fCECdny6vLYVz*Dyz!dw{wGF-ZP0&W~-mTwf$@@+n*Z1C|@7e0!n7;RG z`ktCCr;h4-@6-1@d2Xx8J-+Ni`X2jYUd}7a;?xoS^!s&-uc(Th$lE`l?*;0fn1tN> zpuQKXdtw4|@7MLcNZk{Ak9)tN@5OR&v)Fe`x8KzFkmE;FxxYyaI%LOh>HEck>e1;~ za4eLu`Gsk%hfJgTO>5QdLb3m!+nF_{J~#E=saH?~;M%G4rp(E|oBZv`*G)cavY5PV z@~lZ?;>(-}@VbeoPTV?i!Nl`~VxpT3(RcofTa;yg1 z5dm?!1SK;Cmg{!Z-Zh%)MhP|U>^7^T<^1vdi0GkMMB62B7zK77?X#R+PUj9UoY=!o zk!aw~F7u%L1Zwhw(K|^A@K%KCf@)P?+3Sx4 ztkR#Q6FfVi_ivRbRNgnX3USQ#?1=3WSjz9#G@Y;{wn(%nuUpf2C|#Bz+A8s%>nR?t zErP8Q(n-^?6Fup@Y@0-BG9jP5zDW)Gc8R>CBa0{6PIP3Ggj06*n0wVY%^s@}g3S^g ziJ@KJUO;XiR4VQU33u%5G`HSysAu+`DUpke=w+IOepVqL^`5gqLK6AS={aWnvJ$Tw zCAyHQc80!Hv-%W?6=XK;(07%gzCq&t`0<(Qo|3~GC4`S3pQUfD{`g62e(cmgo>M=b zR!^$UWuwH&@#CHP*6NQBsvpnjyBZ>p2sU`I3k7y*VUd}%a9HtRn}npn?6cQ4)Py!j zm>JzYN4JSE-Y5}cyzwFWR{h{<690vm*sbp_FV?WuMv3R*>2vXkr0n>!YKz(|r=rl8 zJ*#(XG8^TX6CQtPuR*bQlN?&Ys7@-)KBNC?1K1>IkZ^nZn%gYcO>(3Nx3}rr?6GR$ zpC%`S(8E)+j6vq#EGQL*I2k4|ci-|s$hAG2Fn5X#{modt8^2m8qyTi=)CJUWNX z$?x;aRRCkn?=FzzJ35d5!G0%Qbmg3m&OvkP`@CokR#CJ6A~}_#bKKnUgZ--RVEyA$ zr^=uf-mXaotK+C5UaFYM{%%92F-rXMeHcBaV9w%H}KdxT7Sqh``IK}zv zyEU^nN~LoiC;Lk`s7bV0>X!4kyLxx^@mcjaXISdT)#TYMWyN`1%8)Cu-hX(u4cfHmK14l!@#@{x z$InrZKTK@cB?Zj0n74G#=I%+oQCruR?ymc`s9kD%cLpUEL+v>|vvBVTb!|s?Cyjss zKXFoH-HlCMvE9*tpS#+wr-oSWsLan>yrlR2}C(J$E9KdgI5uWI@5o!#2_|Hu1zpeC}dE7m_I$k*3DBfmeRD>i><8vnGu zZT0#~tZUXQS-)TGe!z!s-p>Q8?-v|^_kU~sGy4641n~aCUcz0+9db z6U__9#M64B3uku)CSdIT<^G?kNuALZ;D9&%>wO-Rj-1sM_<#ZX_I;Xb0@GcrKk@hd z&50*48)oa7Bt&Mq7=bdTUs?Z%av3@`owK@P5%Q)l-`_*JFJc$+#OKyOaY%V)<%#L8 zn27Y{Pu4#nZi4zXySid4GVwpN{*e>jX(1%`zd!ot#?)7*K0fs;Q$L0GKbyL2>Z~ba z@=KE+oqYS`^Czpx8zsO}-#327*uRba+1LlieqroIW5>tRv8%^C6#?+~ysG`va-iiv%Yl{yEeBc- zv>a$T&~o61&w;5GV41!hX6IDgD)Sc*%yY(yU@>tI9TCiXEyS^LMF5%I<}LU$IC18n zMqp1_5hP}pd24-FbmmPfg2C)DZ>#Us3irkpL0?u09PSYi%w?DP82zvc(A=~lc*`F1 zvGu)G0s^w^HpjVrBC=}piU2IL=G5xl`tj{604qwI2e$_~sMVJ$ATM?ynsyg27yD-hOi9 z>Ng7-v)g>4c#_ong~OXy1d5q8)7886#Jia;^v^hR2b)6|L}F=v@q^PH~`jPVtLV78gb!3Fu%)QSKvXPNsC z_F`r>u4uqz>mjDnat25s_~wQc0bF*PS3=0rv?o^tYT034CAWmuDJz1q>@p9im+6N# ztq8!f$9zP6ukQb;D}u0`V?I*du5WHy5pZRXc{4XTq51IYvmh%xdyT%W>7KR%q5|l5 zZSUqPV?a{q=XLtNbZg@ZC<=pdy}ngHxN!vpg}XQCyY;-@yaHmv5Z$tWN5h4qtd*9ve*=Ss7<|3_B85Ilr0EafR7ARIq^1q=ii-_x{* z6|8xAZvJ>L67KXB&<|RlQv1QQr=k~bcg70%2SQ=J>r`5m6Yb0u@DE_jJJ$Vpjq~)Z z74Q$B)AhfvA5bJVt$=pw^MxJ9a^ o@uR2frZwyR45@#{cK~WhaN=4QumatstQn(qxBj8_Tnokj50VjuX#fBK literal 0 HcmV?d00001 From b1cf014dc21a2dd61040522df015de5e4b6ed02a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 16:18:39 +0900 Subject: [PATCH 547/587] Add test coverage of EF to Realm migration process --- .../Navigation/TestEFToRealmMigration.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs b/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs new file mode 100644 index 0000000000..00a06d420e --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Models; +using osu.Game.Scoring; +using osu.Game.Skinning; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestEFToRealmMigration : OsuGameTestScene + { + public override void RecycleLocalStorage(bool isDisposing) + { + base.RecycleLocalStorage(isDisposing); + + if (isDisposing) + return; + + using (var outStream = LocalStorage.GetStream(DatabaseContextFactory.DATABASE_NAME, FileAccess.Write, FileMode.Create)) + using (var stream = TestResources.OpenResource(DatabaseContextFactory.DATABASE_NAME)) + stream.CopyTo(outStream); + } + + [Test] + public void TestMigration() + { + // Numbers are taken from the test database (see commit f03de16ee5a46deac3b5f2ca1edfba5c4c5dca7d). + AddAssert("Check beatmaps", () => Game.Dependencies.Get().Run(r => r.All().Count(s => !s.Protected) == 1)); + AddAssert("Check skins", () => Game.Dependencies.Get().Run(r => r.All().Count(s => !s.Protected) == 1)); + AddAssert("Check scores", () => Game.Dependencies.Get().Run(r => r.All().Count() == 1)); + + // One extra file is created during realm migration / startup due to the circles intro import. + AddAssert("Check files", () => Game.Dependencies.Get().Run(r => r.All().Count() == 271)); + } + } +} From d79845fb1df28e63545aa0f4bf9552e16965d9bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 17:06:33 +0900 Subject: [PATCH 548/587] Revert `NUnit3TestAdaptor` (again) Console output is still broken. See https://github.com/ppy/osu/runs/5196023462?check_suite_focus=true. --- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 2bdb6a650c..434c0e0367 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index a6c614f22f..fc6d900567 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index ff49d6d4dd..ddad2adfea 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index a2e54f5cdc..bd4c3d3345 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index debddae037..a6b8eb8651 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 40969c8b29..acf1e8470b 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 9fd73f2c1b..c7314a4969 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe From 03106e846c6a6a70c944253a333aa922c1c2bde7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 17:13:31 +0900 Subject: [PATCH 549/587] Fix test failures due to async mod icon loads --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 4dd3427bee..7c18ed2572 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -173,7 +173,8 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); - AddAssert("mod select contains only double time mod", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Mod is OsuModDoubleTime); + AddUntilStep("mod select contains only double time mod", + () => this.ChildrenOfType().SingleOrDefault()?.ChildrenOfType().SingleOrDefault()?.Mod is OsuModDoubleTime); } } } From b2276baf71bf461c0ba67d2ca6eb634c1ba9299a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 20:55:56 +0900 Subject: [PATCH 550/587] Seal OnlinePlayTestScene.CreateChildDependencies() --- osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 430aae72f8..df2b4f6192 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay }); } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + protected sealed override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { dependencies = new DelegatedDependencyContainer(base.CreateChildDependencies(parent)); return dependencies; From c48a0dc993e4b5621ba9f92625f486d93229ea94 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 20:56:46 +0900 Subject: [PATCH 551/587] Move UserLookupCache to online play test dependencies --- .../Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs | 4 ++-- .../TestSceneMultiplayerGameplayLeaderboardTeams.cs | 2 +- .../Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs | 6 ------ osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 1 - .../Visual/Multiplayer/MultiplayerTestSceneDependencies.cs | 4 ---- .../Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs | 5 +++++ osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs | 1 + .../Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs | 4 ++++ 8 files changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 1322fbc96e..437f4e3308 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).GetResultSafely()); + AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = UserLookupCache.GetUserAsync(1).GetResultSafely()); AddStep("create leaderboard", () => { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestUserQuit() { foreach (int user in users) - AddStep($"mark user {user} quit", () => Client.RemoveUser(LookupCache.GetUserAsync(user).GetResultSafely().AsNonNull())); + AddStep($"mark user {user} quit", () => Client.RemoveUser(UserLookupCache.GetUserAsync(user).GetResultSafely().AsNonNull())); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 8a78c12042..cd3ae50dab 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).GetResultSafely()); + AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = UserLookupCache.GetUserAsync(1).GetResultSafely()); AddStep("create leaderboard", () => { diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs index 204c189591..f166154103 100644 --- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Database; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; @@ -24,11 +23,6 @@ namespace osu.Game.Tests.Visual.Multiplayer /// new TestMultiplayerRoomManager RoomManager { get; } - /// - /// The cached . - /// - TestUserLookupCache LookupCache { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index ed86d572b9..a9b3ca4991 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -19,7 +19,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestMultiplayerClient Client => OnlinePlayDependencies.Client; public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager; - public TestUserLookupCache LookupCache => OnlinePlayDependencies?.LookupCache; public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient; protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs index ed349a7103..d9fe77ae44 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Database; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Screens.OnlinePlay; @@ -16,18 +15,15 @@ namespace osu.Game.Tests.Visual.Multiplayer public class MultiplayerTestSceneDependencies : OnlinePlayTestSceneDependencies, IMultiplayerTestSceneDependencies { public TestMultiplayerClient Client { get; } - public TestUserLookupCache LookupCache { get; } public TestSpectatorClient SpectatorClient { get; } public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager; public MultiplayerTestSceneDependencies() { Client = new TestMultiplayerClient(RoomManager); - LookupCache = new TestUserLookupCache(); SpectatorClient = CreateSpectatorClient(); CacheAs(Client); - CacheAs(LookupCache); CacheAs(SpectatorClient); } diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 71acefb158..feb9b55743 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -31,5 +31,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// The cached . /// OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } + + /// + /// The cached . + /// + TestUserLookupCache UserLookupCache { get; } } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index df2b4f6192..99a492cd6d 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -22,6 +22,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay public IRoomManager RoomManager => OnlinePlayDependencies?.RoomManager; public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies?.OngoingOperationTracker; public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies?.AvailabilityTracker; + public TestUserLookupCache UserLookupCache => OnlinePlayDependencies?.UserLookupCache; /// /// All dependencies required for online play components and screens. diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index 24c4ff79d4..47893519c7 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay; @@ -22,6 +23,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay public OngoingOperationTracker OngoingOperationTracker { get; } public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } public TestRoomRequestsHandler RequestsHandler { get; } + public TestUserLookupCache UserLookupCache { get; } /// /// All cached dependencies which are also components. @@ -38,6 +40,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); RoomManager = CreateRoomManager(); + UserLookupCache = new TestUserLookupCache(); dependencies = new DependencyContainer(new CachedModelDependencyContainer(null) { Model = { BindTarget = SelectedRoom } }); @@ -47,6 +50,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay CacheAs(OngoingOperationTracker); CacheAs(AvailabilityTracker); CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); + CacheAs(UserLookupCache); } public object Get(Type type) From 2675bb87ff3009322be4dcad2e3244ef33425fd3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 21:05:05 +0900 Subject: [PATCH 552/587] Add BeatmapLookupCache as another dependency --- .../Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs | 6 ++++++ osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs | 2 ++ .../Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs | 3 +++ 3 files changed, 11 insertions(+) diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index feb9b55743..c94e288e11 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; @@ -36,5 +37,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// The cached . /// TestUserLookupCache UserLookupCache { get; } + + /// + /// The cached . + /// + BeatmapLookupCache BeatmapLookupCache { get; } } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 99a492cd6d..baff7c168f 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; @@ -23,6 +24,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies?.OngoingOperationTracker; public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies?.AvailabilityTracker; public TestUserLookupCache UserLookupCache => OnlinePlayDependencies?.UserLookupCache; + public BeatmapLookupCache BeatmapLookupCache => OnlinePlayDependencies?.BeatmapLookupCache; /// /// All dependencies required for online play components and screens. diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index 47893519c7..7c8bc2d535 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -24,6 +24,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } public TestRoomRequestsHandler RequestsHandler { get; } public TestUserLookupCache UserLookupCache { get; } + public BeatmapLookupCache BeatmapLookupCache { get; } /// /// All cached dependencies which are also components. @@ -41,6 +42,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); RoomManager = CreateRoomManager(); UserLookupCache = new TestUserLookupCache(); + BeatmapLookupCache = new BeatmapLookupCache(); dependencies = new DependencyContainer(new CachedModelDependencyContainer(null) { Model = { BindTarget = SelectedRoom } }); @@ -51,6 +53,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay CacheAs(AvailabilityTracker); CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); CacheAs(UserLookupCache); + CacheAs(BeatmapLookupCache); } public object Get(Type type) From 539cbe62c6be88f6da5b6cabc1df39f845f8b58b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 21:08:27 +0900 Subject: [PATCH 553/587] Fix incorrect usages of user lookup cache in tests --- .../Visual/Gameplay/TestSceneSpectator.cs | 25 +++++++++---------- .../Visual/Multiplayer/QueueModeTestScene.cs | 4 --- .../TestSceneDrawableRoomPlaylist.cs | 5 +--- .../Multiplayer/TestSceneMultiplayer.cs | 4 --- .../TestSceneMultiplayerQueueList.cs | 4 --- .../TestScenePlaylistsRoomSettingsPlaylist.cs | 8 ++---- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 4 --- .../Visual/TestMultiplayerComponents.cs | 9 +++++++ 8 files changed, 24 insertions(+), 39 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 157c248d69..6dca256d31 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -39,7 +39,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuGameBase game { get; set; } - private TestSpectatorClient spectatorClient; + private TestSpectatorClient spectatorClient => dependenciesScreen.Client; + private DependenciesScreen dependenciesScreen; private SoloSpectator spectatorScreen; private BeatmapSetInfo importedBeatmap; @@ -48,16 +49,16 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUpSteps] public void SetupSteps() { - DependenciesScreen dependenciesScreen = null; - AddStep("load dependencies", () => { - spectatorClient = new TestSpectatorClient(); + LoadScreen(dependenciesScreen = new DependenciesScreen()); - // The screen gets suspended so it stops receiving updates. - Child = spectatorClient; - - LoadScreen(dependenciesScreen = new DependenciesScreen(spectatorClient)); + // The dependencies screen gets suspended so it stops receiving updates. So its children are manually added to the test scene instead. + Children = new Drawable[] + { + dependenciesScreen.UserLookupCache, + dependenciesScreen.Client, + }; }); AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); @@ -335,12 +336,10 @@ namespace osu.Game.Tests.Visual.Gameplay private class DependenciesScreen : OsuScreen { [Cached(typeof(SpectatorClient))] - public readonly TestSpectatorClient Client; + public readonly TestSpectatorClient Client = new TestSpectatorClient(); - public DependenciesScreen(TestSpectatorClient client) - { - Client = client; - } + [Cached(typeof(UserLookupCache))] + public readonly TestUserLookupCache UserLookupCache = new TestUserLookupCache(); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 36d6c6a306..52801dd57a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -10,7 +10,6 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -41,9 +40,6 @@ namespace osu.Game.Tests.Visual.Multiplayer protected TestMultiplayerClient Client => multiplayerComponents.Client; - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestUserLookupCache(); - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 659cc22350..8c10a0d0d9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -30,16 +30,13 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneDrawableRoomPlaylist : OsuManualInputManagerTestScene + public class TestSceneDrawableRoomPlaylist : MultiplayerTestScene { private TestPlaylist playlist; private BeatmapManager manager; private RulesetStore rulesets; - [Cached(typeof(UserLookupCache))] - private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache(); - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 41715f6cfb..b04bf5e860 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -17,7 +17,6 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -56,9 +55,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerClient client => multiplayerComponents.Client; private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager; - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestUserLookupCache(); - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index ddf794b437..4040707c41 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -26,9 +25,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerQueueList : MultiplayerTestScene { - [Cached(typeof(UserLookupCache))] - private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache(); - private MultiplayerQueueList playlist; private BeatmapManager beatmaps; private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index 8bfdda29d5..9adf2c0370 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -4,13 +4,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; -using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Models; using osu.Game.Online.API; @@ -20,18 +18,16 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.OnlinePlay; using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestScenePlaylistsRoomSettingsPlaylist : OsuManualInputManagerTestScene + public class TestScenePlaylistsRoomSettingsPlaylist : OnlinePlayTestScene { private TestPlaylist playlist; - [Cached(typeof(UserLookupCache))] - private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache(); - [Test] public void TestItemRemovedOnDeletion() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 513c1413fa..b66657728e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -10,7 +10,6 @@ using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; @@ -36,9 +35,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerClient client => multiplayerComponents.Client; - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestUserLookupCache(); - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { diff --git a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs index cd7a936778..bd8fb8e58e 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Screens; @@ -39,6 +40,12 @@ namespace osu.Game.Tests.Visual [Cached(typeof(MultiplayerClient))] public readonly TestMultiplayerClient Client; + [Cached(typeof(UserLookupCache))] + private readonly UserLookupCache userLookupCache = new TestUserLookupCache(); + + [Cached] + private readonly BeatmapLookupCache beatmapLookupCache = new BeatmapLookupCache(); + private readonly OsuScreenStack screenStack; private readonly TestMultiplayer multiplayerScreen; @@ -48,6 +55,8 @@ namespace osu.Game.Tests.Visual InternalChildren = new Drawable[] { + userLookupCache, + beatmapLookupCache, Client = new TestMultiplayerClient(RoomManager), screenStack = new OsuScreenStack { From a5183cec77caaa7a8fdb64a8d7a8a7418b7c7f42 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 21:53:56 +0900 Subject: [PATCH 554/587] Add helper to construct APIBeatmap --- osu.Game/Tests/Visual/OsuTestScene.cs | 62 ++++++++++++++++----------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index ec02655544..f287a04d71 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -225,12 +225,24 @@ namespace osu.Game.Tests.Visual protected virtual IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset); /// - /// Returns a sample API Beatmap with BeatmapSet populated. + /// Returns a sample API beatmap with a populated beatmap set. /// /// The ruleset to create the sample model using. osu! ruleset will be used if not specified. - protected APIBeatmap CreateAPIBeatmap(RulesetInfo ruleset = null) + protected APIBeatmap CreateAPIBeatmap(RulesetInfo ruleset = null) => CreateAPIBeatmap(CreateBeatmap(ruleset ?? Ruleset.Value).BeatmapInfo); + + /// + /// Constructs a sample API beatmap set containing a beatmap. + /// + /// The ruleset to create the sample model using. osu! ruleset will be used if not specified. + protected APIBeatmapSet CreateAPIBeatmapSet(RulesetInfo ruleset = null) => CreateAPIBeatmapSet(CreateBeatmap(ruleset ?? Ruleset.Value).BeatmapInfo); + + /// + /// Constructs a sample API beatmap with a populated beatmap set from a given source beatmap. + /// + /// The source beatmap. + public static APIBeatmap CreateAPIBeatmap(IBeatmapInfo original) { - var beatmapSet = CreateAPIBeatmapSet(ruleset ?? Ruleset.Value); + var beatmapSet = CreateAPIBeatmapSet(original); // Avoid circular reference. var beatmap = beatmapSet.Beatmaps.First(); @@ -243,18 +255,16 @@ namespace osu.Game.Tests.Visual } /// - /// Returns a sample API BeatmapSet with beatmaps populated. + /// Constructs a sample API beatmap set containing a beatmap from a given source beatmap. /// - /// The ruleset to create the sample model using. osu! ruleset will be used if not specified. - protected APIBeatmapSet CreateAPIBeatmapSet(RulesetInfo ruleset = null) + /// The source beatmap. + public static APIBeatmapSet CreateAPIBeatmapSet(IBeatmapInfo original) { - var beatmap = CreateBeatmap(ruleset ?? Ruleset.Value).BeatmapInfo; - - Debug.Assert(beatmap.BeatmapSet != null); + Debug.Assert(original.BeatmapSet != null); return new APIBeatmapSet { - OnlineID = ((IBeatmapSetInfo)beatmap.BeatmapSet).OnlineID, + OnlineID = original.BeatmapSet.OnlineID, Status = BeatmapOnlineStatus.Ranked, Covers = new BeatmapSetOnlineCovers { @@ -262,29 +272,29 @@ namespace osu.Game.Tests.Visual Card = "https://assets.ppy.sh/beatmaps/163112/covers/card.jpg", List = "https://assets.ppy.sh/beatmaps/163112/covers/list.jpg" }, - Title = beatmap.Metadata.Title, - TitleUnicode = beatmap.Metadata.TitleUnicode, - Artist = beatmap.Metadata.Artist, - ArtistUnicode = beatmap.Metadata.ArtistUnicode, + Title = original.Metadata.Title, + TitleUnicode = original.Metadata.TitleUnicode, + Artist = original.Metadata.Artist, + ArtistUnicode = original.Metadata.ArtistUnicode, Author = new APIUser { - Username = beatmap.Metadata.Author.Username, - Id = beatmap.Metadata.Author.OnlineID + Username = original.Metadata.Author.Username, + Id = original.Metadata.Author.OnlineID }, - Source = beatmap.Metadata.Source, - Tags = beatmap.Metadata.Tags, + Source = original.Metadata.Source, + Tags = original.Metadata.Tags, Beatmaps = new[] { new APIBeatmap { - OnlineID = ((IBeatmapInfo)beatmap).OnlineID, - OnlineBeatmapSetID = ((IBeatmapSetInfo)beatmap.BeatmapSet).OnlineID, - Status = beatmap.Status, - Checksum = beatmap.MD5Hash, - AuthorID = beatmap.Metadata.Author.OnlineID, - RulesetID = beatmap.Ruleset.OnlineID, - StarRating = beatmap.StarRating, - DifficultyName = beatmap.DifficultyName, + OnlineID = original.OnlineID, + OnlineBeatmapSetID = original.BeatmapSet.OnlineID, + Status = ((BeatmapInfo)original).Status, + Checksum = original.MD5Hash, + AuthorID = original.Metadata.Author.OnlineID, + RulesetID = original.Ruleset.OnlineID, + StarRating = original.StarRating, + DifficultyName = original.DifficultyName, } } }; From ccd265ebe7237e50ecb7241dca5e3f3703b47a86 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 22:02:33 +0900 Subject: [PATCH 555/587] Handle beatmap lookup requests in TestRoomRequestsHandler --- .../Visual/TestMultiplayerComponents.cs | 8 ++++-- .../Online/API/Requests/GetBeatmapsRequest.cs | 6 ++-- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 11 +++++--- .../OnlinePlay/TestRoomRequestsHandler.cs | 28 +++++++++++++++++-- 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs index bd8fb8e58e..2e551947b6 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Database; +using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Screens; @@ -46,6 +47,9 @@ namespace osu.Game.Tests.Visual [Cached] private readonly BeatmapLookupCache beatmapLookupCache = new BeatmapLookupCache(); + [Resolved] + private BeatmapManager beatmapManager { get; set; } + private readonly OsuScreenStack screenStack; private readonly TestMultiplayer multiplayerScreen; @@ -69,9 +73,9 @@ namespace osu.Game.Tests.Visual } [BackgroundDependencyLoader] - private void load(IAPIProvider api, OsuGameBase game) + private void load(IAPIProvider api) { - ((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, game); + ((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, beatmapManager); } public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton(); diff --git a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs index 1d71e22b77..c07e5ef1c2 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs @@ -7,7 +7,7 @@ namespace osu.Game.Online.API.Requests { public class GetBeatmapsRequest : APIRequest { - private readonly int[] beatmapIds; + public readonly int[] BeatmapIds; private const int max_ids_per_request = 50; @@ -16,9 +16,9 @@ namespace osu.Game.Online.API.Requests if (beatmapIds.Length > max_ids_per_request) throw new ArgumentException($"{nameof(GetBeatmapsRequest)} calls only support up to {max_ids_per_request} IDs at once"); - this.beatmapIds = beatmapIds; + BeatmapIds = beatmapIds; } - protected override string Target => "beatmaps/?ids[]=" + string.Join("&ids[]=", beatmapIds); + protected override string Target => "beatmaps/?ids[]=" + string.Join("&ids[]=", BeatmapIds); } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index baff7c168f..448ec5e3d9 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Database; +using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; @@ -33,9 +34,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay protected override Container Content => content; - [Resolved] - private OsuGameBase game { get; set; } - private readonly Container content; private readonly Container drawableDependenciesContainer; private DelegatedDependencyContainer dependencies; @@ -71,7 +69,12 @@ namespace osu.Game.Tests.Visual.OnlinePlay AddStep("setup API", () => { var handler = OnlinePlayDependencies.RequestsHandler; - ((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, game); + + // Resolving the BeatmapManager in the test scene will inject the game-wide BeatmapManager, while many test scenes cache their own BeatmapManager instead. + // To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead. + var beatmapManager = dependencies.Get(); + + ((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, beatmapManager); }); } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 5a0a7e71d4..8290af8f78 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -4,12 +4,16 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.OnlinePlay { @@ -33,9 +37,9 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// /// The API request to handle. /// The local user to store in responses where required. - /// The game base for cases where actual online requests need to be sent. + /// The beatmap manager to attempt to retrieve beatmaps from, prior to returning dummy beatmaps. /// Whether the request was successfully handled. - public bool HandleRequest(APIRequest request, APIUser localUser, OsuGameBase game) + public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager) { switch (request) { @@ -128,6 +132,26 @@ namespace osu.Game.Tests.Visual.OnlinePlay Statistics = new Dictionary() }); return true; + + case GetBeatmapsRequest getBeatmapsRequest: + var result = new List(); + + foreach (int id in getBeatmapsRequest.BeatmapIds) + { + var baseBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id); + + if (baseBeatmap == null) + { + baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo; + baseBeatmap.OnlineID = id; + baseBeatmap.BeatmapSet!.OnlineID = id; + } + + result.Add(OsuTestScene.CreateAPIBeatmap(baseBeatmap)); + } + + getBeatmapsRequest.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = result }); + return true; } return false; From 73ce1b324e84e5eef58ddf9ef5010e69a36938c7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 22:44:20 +0900 Subject: [PATCH 556/587] Make DrawableRoom look up the online beatmap --- .../Lounge/Components/DrawableRoom.cs | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index a87f21630c..60f65fbd9f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -12,6 +14,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -328,6 +331,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved] private OsuColour colours { get; set; } + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } + private SpriteText statusText; private LinkFlowContainer beatmapText; @@ -385,8 +391,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components SelectedItem.BindValueChanged(onSelectedItemChanged, true); } + private CancellationTokenSource beatmapLookupCancellation; + private void onSelectedItemChanged(ValueChangedEvent item) { + beatmapLookupCancellation?.Cancel(); beatmapText.Clear(); if (Type.Value == MatchType.Playlists) @@ -395,17 +404,25 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return; } - if (item.NewValue?.Beatmap.Value != null) - { - statusText.Text = "Currently playing "; - beatmapText.AddLink(item.NewValue.Beatmap.Value.GetDisplayTitleRomanisable(), - LinkAction.OpenBeatmap, - item.NewValue.Beatmap.Value.OnlineID.ToString(), - creationParameters: s => - { - s.Truncate = true; - }); - } + var beatmap = item.NewValue?.Beatmap; + if (beatmap == null) + return; + + var cancellationSource = beatmapLookupCancellation = new CancellationTokenSource(); + beatmapLookupCache.GetBeatmapAsync(beatmap.Value.OnlineID, cancellationSource.Token) + .ContinueWith(task => Schedule(() => + { + if (cancellationSource.IsCancellationRequested) + return; + + var retrievedBeatmap = task.GetResultSafely(); + + statusText.Text = "Currently playing "; + beatmapText.AddLink(retrievedBeatmap.GetDisplayTitleRomanisable(), + LinkAction.OpenBeatmap, + retrievedBeatmap.OnlineID.ToString(), + creationParameters: s => s.Truncate = true); + }), cancellationSource.Token); } } From afcb7a463061756988648e60f06a0ad9173f43ba Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:11:33 +0900 Subject: [PATCH 557/587] Make DrawableRoomPlaylistItem look up the online beatmap --- .../OnlinePlayBeatmapAvailabilityTracker.cs | 23 ++-- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 102 ++++++++---------- 2 files changed, 57 insertions(+), 68 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index c67cbade6a..51e77694a1 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -78,7 +78,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(r => filteredBeatmaps(), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap.Value), (items, changes, ___) => { if (changes == null) return; @@ -108,12 +108,12 @@ namespace osu.Game.Online.Rooms break; case DownloadState.LocallyAvailable: - bool hashMatches = filteredBeatmaps().Any(); + bool available = QueryBeatmapForOnlinePlay(realm.Realm, SelectedItem.Value.Beatmap.Value).Any(); - availability.Value = hashMatches ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); + availability.Value = available ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); // only display a message to the user if a download seems to have just completed. - if (!hashMatches && downloadTracker.Progress.Value == 1) + if (!available && downloadTracker.Progress.Value == 1) Logger.Log("The imported beatmap set does not match the online version.", LoggingTarget.Runtime, LogLevel.Important); break; @@ -123,14 +123,15 @@ namespace osu.Game.Online.Rooms } } - private IQueryable filteredBeatmaps() + /// + /// Performs a query for a local matching a requested one for the purpose of online play. + /// + /// The realm to query from. + /// The requested beatmap. + /// A beatmap query. + public static IQueryable QueryBeatmapForOnlinePlay(Realm realm, IBeatmapInfo beatmap) { - int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID; - string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; - - return realm.Realm - .All() - .Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum); + return realm.All().Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", beatmap.OnlineID, beatmap.MD5Hash); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index dcf2a5a480..7674fac88e 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -25,7 +24,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.Chat; -using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays.BeatmapSet; using osu.Game.Rulesets; @@ -68,8 +66,8 @@ namespace osu.Game.Screens.OnlinePlay private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; private readonly IBindable valid = new Bindable(); - private readonly Bindable beatmap = new Bindable(); + private IBeatmapInfo beatmap; private IRulesetInfo ruleset; private Mod[] requiredMods; @@ -96,13 +94,12 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private UserLookupCache userLookupCache { get; set; } - [CanBeNull] - [Resolved(CanBeNull = true)] - private MultiplayerClient multiplayerClient { get; set; } - [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } + [Resolved] + private RealmAccess realm { get; set; } + protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model; public DrawableRoomPlaylistItem(PlaylistItem item) @@ -110,7 +107,6 @@ namespace osu.Game.Screens.OnlinePlay { Item = item; - beatmap.BindTo(item.Beatmap); valid.BindTo(item.Valid); if (item.Expired) @@ -151,7 +147,6 @@ namespace osu.Game.Screens.OnlinePlay maskingContainer.BorderThickness = isCurrent ? 5 : 0; }, true); - beatmap.BindValueChanged(_ => Scheduler.AddOnce(refresh)); valid.BindValueChanged(_ => Scheduler.AddOnce(refresh)); onScreenLoader.DelayedLoadStarted += _ => @@ -166,19 +161,9 @@ namespace osu.Game.Screens.OnlinePlay Schedule(() => ownerAvatar.User = foundUser); } - if (Item.Beatmap.Value == null) - { - IBeatmapInfo foundBeatmap; + beatmap = await beatmapLookupCache.GetBeatmapAsync(Item.Beatmap.Value.OnlineID).ConfigureAwait(false); - if (multiplayerClient != null) - // This call can eventually go away (and use the else case below). - // Currently required only due to the method being overridden to provide special behaviour in tests. - foundBeatmap = await multiplayerClient.GetAPIBeatmap(Item.BeatmapID).ConfigureAwait(false); - else - foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false); - - Schedule(() => Item.Beatmap.Value = foundBeatmap); - } + Scheduler.AddOnce(refresh); } catch (Exception e) { @@ -280,18 +265,18 @@ namespace osu.Game.Screens.OnlinePlay maskingContainer.BorderColour = colours.Red; } - if (Item.Beatmap.Value != null) - difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; + if (beatmap != null) + difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; else difficultyIconContainer.Clear(); - panelBackground.Beatmap.Value = Item.Beatmap.Value; + panelBackground.Beatmap.Value = beatmap; beatmapText.Clear(); - if (Item.Beatmap.Value != null) + if (beatmap != null) { - beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineID.ToString(), null, text => + beatmapText.AddLink(beatmap.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, beatmap.OnlineID.ToString(), null, text => { text.Truncate = true; }); @@ -299,13 +284,13 @@ namespace osu.Game.Screens.OnlinePlay authorText.Clear(); - if (!string.IsNullOrEmpty(Item.Beatmap.Value?.Metadata.Author.Username)) + if (!string.IsNullOrEmpty(beatmap?.Metadata.Author.Username)) { authorText.AddText("mapped by "); - authorText.AddUserLink(Item.Beatmap.Value.Metadata.Author); + authorText.AddUserLink(beatmap.Metadata.Author); } - bool hasExplicitContent = (Item.Beatmap.Value?.BeatmapSet as IBeatmapSetOnlineInfo)?.HasExplicitContent == true; + bool hasExplicitContent = (beatmap?.BeatmapSet as IBeatmapSetOnlineInfo)?.HasExplicitContent == true; explicitContentPill.Alpha = hasExplicitContent ? 1 : 0; modDisplay.Current.Value = requiredMods.ToArray(); @@ -448,31 +433,34 @@ namespace osu.Game.Screens.OnlinePlay }; } - private IEnumerable createButtons() => new[] + private IEnumerable createButtons() { - showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) + return new[] { - Size = new Vector2(30, 30), - Action = () => RequestResults?.Invoke(Item), - Alpha = AllowShowingResults ? 1 : 0, - TooltipText = "View results" - }, - Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item), - editButton = new PlaylistEditButton - { - Size = new Vector2(30, 30), - Alpha = AllowEditing ? 1 : 0, - Action = () => RequestEdit?.Invoke(Item), - TooltipText = "Edit" - }, - removeButton = new PlaylistRemoveButton - { - Size = new Vector2(30, 30), - Alpha = AllowDeletion ? 1 : 0, - Action = () => RequestDeletion?.Invoke(Item), - TooltipText = "Remove from playlist" - }, - }; + showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) + { + Size = new Vector2(30, 30), + Action = () => RequestResults?.Invoke(Item), + Alpha = AllowShowingResults ? 1 : 0, + TooltipText = "View results" + }, + OnlinePlayBeatmapAvailabilityTracker.QueryBeatmapForOnlinePlay(realm.Realm, beatmap).Any() ? Empty() : new PlaylistDownloadButton(beatmap), + editButton = new PlaylistEditButton + { + Size = new Vector2(30, 30), + Alpha = AllowEditing ? 1 : 0, + Action = () => RequestEdit?.Invoke(Item), + TooltipText = "Edit" + }, + removeButton = new PlaylistRemoveButton + { + Size = new Vector2(30, 30), + Alpha = AllowDeletion ? 1 : 0, + Action = () => RequestDeletion?.Invoke(Item), + TooltipText = "Remove from playlist" + }, + }; + } protected override bool OnClick(ClickEvent e) { @@ -499,7 +487,7 @@ namespace osu.Game.Screens.OnlinePlay private sealed class PlaylistDownloadButton : BeatmapDownloadButton { - private readonly PlaylistItem playlistItem; + private readonly IBeatmapInfo beatmap; [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -509,10 +497,10 @@ namespace osu.Game.Screens.OnlinePlay private const float width = 50; - public PlaylistDownloadButton(PlaylistItem playlistItem) - : base(playlistItem.Beatmap.Value.BeatmapSet) + public PlaylistDownloadButton(IBeatmapInfo beatmap) + : base(beatmap.BeatmapSet) { - this.playlistItem = playlistItem; + this.beatmap = beatmap; Size = new Vector2(width, 30); Alpha = 0; @@ -532,7 +520,7 @@ namespace osu.Game.Screens.OnlinePlay { case DownloadState.LocallyAvailable: // Perform a local query of the beatmap by beatmap checksum, and reset the state if not matching. - if (beatmapManager.QueryBeatmap(b => b.MD5Hash == playlistItem.Beatmap.Value.MD5Hash) == null) + if (beatmapManager.QueryBeatmap(b => b.MD5Hash == beatmap.MD5Hash) == null) State.Value = DownloadState.NotDownloaded; else { From 94a974e1c97ea36318488ba9c4c17db1d7ea9c67 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:30:39 +0900 Subject: [PATCH 558/587] Make OnlinePlayBeatmapAvailabilityTracker look up the online beatmap --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 57 ++++++++++++--- .../OnlinePlayBeatmapAvailabilityTracker.cs | 73 ++++++++++++------- 2 files changed, 93 insertions(+), 37 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 9aa04dda92..4b98385a51 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; @@ -12,6 +13,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Graphics; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Framework.Testing; @@ -21,6 +23,8 @@ 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.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Tests.Resources; @@ -53,6 +57,25 @@ namespace osu.Game.Tests.Online [SetUp] public void SetUp() => Schedule(() => { + ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case GetBeatmapsRequest beatmapsReq: + var beatmap = CreateAPIBeatmap(); + beatmap.OnlineID = testBeatmapInfo.OnlineID; + beatmap.OnlineBeatmapSetID = testBeatmapSet.OnlineID; + beatmap.Checksum = testBeatmapInfo.MD5Hash; + beatmap.BeatmapSet!.OnlineID = testBeatmapSet.OnlineID; + + beatmapsReq.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = new List { beatmap } }); + return true; + + default: + return false; + } + }; + beatmaps.AllowImport = new TaskCompletionSource(); testBeatmapFile = TestResources.GetQuickTestBeatmapForImport(); @@ -69,12 +92,30 @@ namespace osu.Game.Tests.Online RulesetID = testBeatmapInfo.Ruleset.OnlineID, }; - Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker - { - SelectedItem = { BindTarget = selectedItem, } - }; + recreateChildren(); }); + private void recreateChildren() + { + var beatmapLookupCache = new BeatmapLookupCache(); + + Child = new DependencyProvidingContainer + { + CachedDependencies = new[] + { + (typeof(BeatmapLookupCache), (object)beatmapLookupCache) + }, + Children = new Drawable[] + { + beatmapLookupCache, + availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker + { + SelectedItem = { BindTarget = selectedItem, } + } + } + }; + } + [Test] public void TestBeatmapDownloadingFlow() { @@ -123,10 +164,7 @@ namespace osu.Game.Tests.Online }); addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded); - AddStep("recreate tracker", () => Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker - { - SelectedItem = { BindTarget = selectedItem } - }); + AddStep("recreate tracker", recreateChildren); addAvailabilityCheckStep("state not downloaded as well", BeatmapAvailability.NotDownloaded); AddStep("reimport original beatmap", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely()); @@ -167,7 +205,8 @@ namespace osu.Game.Tests.Online public Live CurrentImport { get; private set; } - public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) + public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, + GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap) { } diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 51e77694a1..90532d88b8 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -4,14 +4,17 @@ using System; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Online.API.Requests.Responses; using Realms; namespace osu.Game.Online.Rooms @@ -32,6 +35,9 @@ namespace osu.Game.Online.Rooms [Resolved] private RealmAccess realm { get; set; } = null!; + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + /// /// The availability state of the currently selected playlist item. /// @@ -58,39 +64,50 @@ namespace osu.Game.Online.Rooms downloadTracker?.RemoveAndDisposeImmediately(); - Debug.Assert(item.NewValue.Beatmap.Value.BeatmapSet != null); - - downloadTracker = new BeatmapDownloadTracker(item.NewValue.Beatmap.Value.BeatmapSet); - - AddInternal(downloadTracker); - - downloadTracker.State.BindValueChanged(_ => Scheduler.AddOnce(updateAvailability), true); - downloadTracker.Progress.BindValueChanged(_ => + beatmapLookupCache.GetBeatmapAsync(item.NewValue.Beatmap.Value.OnlineID).ContinueWith(task => Schedule(() => { - if (downloadTracker.State.Value != DownloadState.Downloading) - return; + var beatmap = task.GetResultSafely(); - // incoming progress changes are going to be at a very high rate. - // we don't want to flood the network with this, so rate limit how often we send progress updates. - if (progressUpdate?.Completed != false) - progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); - }, true); - - // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). - realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap.Value), (items, changes, ___) => - { - if (changes == null) - return; - - Scheduler.AddOnce(updateAvailability); - }); + if (SelectedItem.Value?.Beatmap.Value.OnlineID == beatmap.OnlineID) + beginTracking(beatmap); + }), TaskContinuationOptions.OnlyOnRanToCompletion); }, true); } - private void updateAvailability() + private void beginTracking(APIBeatmap beatmap) { - if (downloadTracker == null || SelectedItem.Value == null) + Debug.Assert(beatmap.BeatmapSet != null); + + downloadTracker = new BeatmapDownloadTracker(beatmap.BeatmapSet); + + AddInternal(downloadTracker); + + downloadTracker.State.BindValueChanged(_ => Scheduler.AddOnce(updateAvailability, beatmap), true); + downloadTracker.Progress.BindValueChanged(_ => + { + if (downloadTracker.State.Value != DownloadState.Downloading) + return; + + // incoming progress changes are going to be at a very high rate. + // we don't want to flood the network with this, so rate limit how often we send progress updates. + if (progressUpdate?.Completed != false) + progressUpdate = Scheduler.AddDelayed(updateAvailability, beatmap, progressUpdate == null ? 0 : 500); + }, true); + + // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). + realmSubscription?.Dispose(); + realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap.Value), (items, changes, ___) => + { + if (changes == null) + return; + + Scheduler.AddOnce(updateAvailability, beatmap); + }); + } + + private void updateAvailability(APIBeatmap beatmap) + { + if (downloadTracker == null) return; switch (downloadTracker.State.Value) @@ -108,7 +125,7 @@ namespace osu.Game.Online.Rooms break; case DownloadState.LocallyAvailable: - bool available = QueryBeatmapForOnlinePlay(realm.Realm, SelectedItem.Value.Beatmap.Value).Any(); + bool available = QueryBeatmapForOnlinePlay(realm.Realm, beatmap).Any(); availability.Value = available ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); From bdc3b76df0b58406796e2b08db13be7f2140fa7e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:33:26 +0900 Subject: [PATCH 559/587] Remove beatmap bindable from PlaylistItem --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 3 +- .../OnlinePlay/PlaylistExtensionsTest.cs | 25 ++--- .../Visual/Multiplayer/QueueModeTestScene.cs | 3 +- .../Multiplayer/TestSceneDrawableRoom.cs | 68 ++++-------- .../TestSceneDrawableRoomPlaylist.cs | 37 +++---- .../Multiplayer/TestSceneHostOnlyQueueMode.cs | 8 +- .../TestSceneMatchBeatmapDetailArea.cs | 3 +- .../Multiplayer/TestSceneMultiplayer.cs | 101 +++++++----------- .../TestSceneMultiplayerMatchSubScreen.cs | 15 +-- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 3 +- .../TestSceneMultiplayerPlaylist.cs | 12 +-- .../TestSceneMultiplayerQueueList.cs | 6 +- .../TestSceneMultiplayerReadyButton.cs | 3 +- .../TestSceneMultiplayerResults.cs | 7 +- .../TestSceneMultiplayerSpectateButton.cs | 3 +- .../TestSceneMultiplayerTeamResults.cs | 7 +- .../TestScenePlaylistsRoomSettingsPlaylist.cs | 38 +++---- .../TestSceneStarRatingRangeDisplay.cs | 4 +- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 16 ++- .../TestScenePlaylistsMatchSettingsOverlay.cs | 8 +- .../TestScenePlaylistsResultsScreen.cs | 3 +- .../TestScenePlaylistsRoomCreation.cs | 31 +++--- .../Online/Multiplayer/MultiplayerClient.cs | 11 +- .../Multiplayer/OnlineMultiplayerClient.cs | 10 -- .../Online/Rooms/MultiplayerPlaylistItem.cs | 4 +- .../OnlinePlayBeatmapAvailabilityTracker.cs | 6 +- osu.Game/Online/Rooms/PlaylistExtensions.cs | 2 +- osu.Game/Online/Rooms/PlaylistItem.cs | 74 +++++++++---- .../OnlinePlay/Components/BeatmapTitle.cs | 4 +- .../OnlinePlay/Components/ModeTypeInfo.cs | 2 +- .../Components/OnlinePlayBackgroundScreen.cs | 2 +- .../Components/OnlinePlayBackgroundSprite.cs | 2 +- .../Components/PlaylistItemBackground.cs | 2 +- .../OnlinePlay/Components/RoomManager.cs | 3 - .../Components/StarRatingRangeDisplay.cs | 2 +- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- .../Lounge/Components/DrawableRoom.cs | 2 +- .../OnlinePlay/Match/DrawableMatchRoom.cs | 2 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- .../Multiplayer/MultiplayerMatchSongSelect.cs | 4 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 34 +----- .../OnlinePlay/OnlinePlaySongSelect.cs | 6 +- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +- .../Playlists/PlaylistsRoomSettingsOverlay.cs | 2 +- .../Playlists/PlaylistsSongSelect.cs | 3 +- .../Multiplayer/MultiplayerTestScene.cs | 3 +- .../Multiplayer/TestMultiplayerClient.cs | 23 ---- .../Visual/OnlinePlay/TestRoomManager.cs | 9 +- 48 files changed, 227 insertions(+), 395 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 4b98385a51..dd7feb6699 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -86,9 +86,8 @@ namespace osu.Game.Tests.Online Realm.Write(r => r.RemoveAll()); Realm.Write(r => r.RemoveAll()); - selectedItem.Value = new PlaylistItem + selectedItem.Value = new PlaylistItem(testBeatmapInfo) { - Beatmap = { Value = testBeatmapInfo }, RulesetID = testBeatmapInfo.Ruleset.OnlineID, }; diff --git a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs index d33081662d..9e7ea02101 100644 --- a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs +++ b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs @@ -3,6 +3,7 @@ using System; using NUnit.Framework; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; namespace osu.Game.Tests.OnlinePlay @@ -29,9 +30,9 @@ namespace osu.Game.Tests.OnlinePlay { var items = new[] { - new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1 }, - new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2 }, - new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, PlaylistOrder = 1 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, PlaylistOrder = 2 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1003 }) { ID = 3, PlaylistOrder = 3 }, }; Assert.Multiple(() => @@ -47,9 +48,9 @@ namespace osu.Game.Tests.OnlinePlay { var items = new[] { - new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2 }, - new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1 }, - new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, PlaylistOrder = 2 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, PlaylistOrder = 1 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1003 }) { ID = 3, PlaylistOrder = 3 }, }; Assert.Multiple(() => @@ -65,9 +66,9 @@ namespace osu.Game.Tests.OnlinePlay { var items = new[] { - new PlaylistItem { ID = 1, BeatmapID = 1001, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) }, - new PlaylistItem { ID = 2, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) }, - new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) }, + new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) }, + new PlaylistItem(new APIBeatmap { OnlineID = 1003 }) { ID = 3, PlaylistOrder = 3 }, }; Assert.Multiple(() => @@ -83,9 +84,9 @@ namespace osu.Game.Tests.OnlinePlay { var items = new[] { - new PlaylistItem { ID = 1, BeatmapID = 1001, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) }, - new PlaylistItem { ID = 2, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) }, - new PlaylistItem { ID = 3, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 57, 0, TimeSpan.Zero) }, + new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) }, + new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) }, + new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 3, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 57, 0, TimeSpan.Zero) }, }; Assert.Multiple(() => diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 52801dd57a..5a211e7bec 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -71,9 +71,8 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = Mode }, Playlist = { - new PlaylistItem + new PlaylistItem(InitialBeatmap) { - Beatmap = { Value = InitialBeatmap }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index 423822cbe4..d8ec0ad1f0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -53,19 +53,13 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.HeadToHead }, Playlist = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) { - Beatmap = + BeatmapInfo = { - Value = new TestBeatmap(new OsuRuleset().RulesetInfo) - { - BeatmapInfo = - { - StarRating = 2.5 - } - }.BeatmapInfo, + StarRating = 2.5 } - } + }.BeatmapInfo) } }), createLoungeRoom(new Room @@ -76,26 +70,20 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.HeadToHead }, Playlist = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) { - Beatmap = + BeatmapInfo = { - Value = new TestBeatmap(new OsuRuleset().RulesetInfo) + StarRating = 2.5, + Metadata = { - BeatmapInfo = - { - StarRating = 2.5, - Metadata = - { - Artist = "very very very very very very very very very long artist", - ArtistUnicode = "very very very very very very very very very long artist", - Title = "very very very very very very very very very very very long title", - TitleUnicode = "very very very very very very very very very very very long title", - } - } - }.BeatmapInfo, + Artist = "very very very very very very very very very long artist", + ArtistUnicode = "very very very very very very very very very long artist", + Title = "very very very very very very very very very very very long title", + TitleUnicode = "very very very very very very very very very very very long title", + } } - } + }.BeatmapInfo) } }), createLoungeRoom(new Room @@ -105,32 +93,20 @@ namespace osu.Game.Tests.Visual.Multiplayer EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, Playlist = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) { - Beatmap = + BeatmapInfo = { - Value = new TestBeatmap(new OsuRuleset().RulesetInfo) - { - BeatmapInfo = - { - StarRating = 2.5 - } - }.BeatmapInfo, + StarRating = 2.5 } - }, - new PlaylistItem + }.BeatmapInfo), + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) { - Beatmap = + BeatmapInfo = { - Value = new TestBeatmap(new OsuRuleset().RulesetInfo) - { - BeatmapInfo = - { - StarRating = 4.5 - } - }.BeatmapInfo, + StarRating = 4.5 } - } + }.BeatmapInfo) } }), createLoungeRoom(new Room diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 8c10a0d0d9..bea15e6e58 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -209,10 +209,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Size = new Vector2(500, 300), Items = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { ID = 0, - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, Expired = true, RequiredMods = new[] @@ -222,10 +221,9 @@ namespace osu.Game.Tests.Visual.Multiplayer new APIMod(new OsuModAutoplay()) } }, - new PlaylistItem + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { ID = 1, - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { @@ -293,25 +291,21 @@ namespace osu.Game.Tests.Visual.Multiplayer for (int i = 0; i < 20; i++) { - playlist.Items.Add(new PlaylistItem + playlist.Items.Add(new PlaylistItem(i % 2 == 1 + ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo + : new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = "Artist", + Author = new RealmUser { Username = "Creator name here" }, + Title = "Long title used to check background colour", + }, + BeatmapSet = new BeatmapSetInfo() + }) { ID = i, OwnerID = 2, - Beatmap = - { - Value = i % 2 == 1 - ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo - : new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Artist = "Artist", - Author = new RealmUser { Username = "Creator name here" }, - Title = "Long title used to check background colour", - }, - BeatmapSet = new BeatmapSetInfo() - } - }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { @@ -341,11 +335,10 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (var b in beatmaps()) { - playlist.Items.Add(new PlaylistItem + playlist.Items.Add(new PlaylistItem(b) { ID = index++, OwnerID = 2, - Beatmap = { Value = b }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index c7eeff81fe..7614e52218 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -57,12 +57,12 @@ namespace osu.Game.Tests.Visual.Multiplayer RunGameplay(); IBeatmapInfo firstBeatmap = null; - AddStep("get first playlist item beatmap", () => firstBeatmap = Client.APIRoom?.Playlist[0].Beatmap.Value); + AddStep("get first playlist item beatmap", () => firstBeatmap = Client.APIRoom?.Playlist[0].Beatmap); selectNewItem(() => OtherBeatmap); - AddAssert("first playlist item hasn't changed", () => Client.APIRoom?.Playlist[0].Beatmap.Value == firstBeatmap); - AddAssert("second playlist item changed", () => Client.APIRoom?.Playlist[1].Beatmap.Value != firstBeatmap); + AddAssert("first playlist item hasn't changed", () => Client.APIRoom?.Playlist[0].Beatmap == firstBeatmap); + AddAssert("second playlist item changed", () => Client.APIRoom?.Playlist[1].Beatmap != firstBeatmap); } [Test] @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap())); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); - AddUntilStep("selected item is new beatmap", () => (CurrentSubScreen as MultiplayerMatchSubScreen)?.SelectedItem.Value?.BeatmapID == otherBeatmap.OnlineID); + AddUntilStep("selected item is new beatmap", () => (CurrentSubScreen as MultiplayerMatchSubScreen)?.SelectedItem.Value?.Beatmap.OnlineID == otherBeatmap.OnlineID); } private void addItem(Func beatmap) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 6144824ba0..6f43511e8a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -32,10 +32,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createNewItem() { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { ID = SelectedRoom.Value.Playlist.Count, - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index b04bf5e860..4d87480841 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -93,9 +93,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -229,9 +228,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -251,9 +249,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -281,9 +278,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -312,9 +308,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Password = { Value = "password" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -334,9 +329,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Password = { Value = "password" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -367,9 +361,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Password = { Value = "password" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -387,9 +380,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -409,9 +401,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -448,9 +439,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -487,9 +477,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -526,9 +515,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -560,9 +548,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -600,9 +587,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -620,9 +606,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, AllowedMods = new[] { new APIMod(new OsuModHidden()) } } @@ -660,10 +645,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -691,10 +675,9 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = QueueMode.AllPlayers }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }, API.LocalUser.Value); @@ -708,11 +691,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change server-side settings", () => { roomManager.ServerSideRooms[0].Name.Value = "New name"; - roomManager.ServerSideRooms[0].Playlist.Add(new PlaylistItem + roomManager.ServerSideRooms[0].Playlist.Add(new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { ID = 2, - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, }); }); @@ -737,10 +719,9 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = QueueMode.AllPlayers }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -773,10 +754,9 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = QueueMode.AllPlayers }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -812,10 +792,9 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = QueueMode.AllPlayers }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -823,10 +802,11 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem - { - BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID - }))); + AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem( + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + }))); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); @@ -843,10 +823,9 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = QueueMode.AllPlayers }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -854,10 +833,11 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem - { - BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID - }))); + AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem( + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + }))); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); @@ -876,9 +856,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 7c18ed2572..e137bf83e4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -73,9 +73,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -90,9 +89,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, AllowedMods = new[] { new APIMod(new TaikoModSwap()) } }); @@ -113,9 +111,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set playlist", () => { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -128,9 +125,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set playlist", () => { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -159,9 +155,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item with allowed mod", () => { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, AllowedMods = new[] { new APIMod(new OsuModDoubleTime()) } }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 010e9dc078..c402aff771 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -28,9 +28,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("initialise gameplay", () => { - Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem + Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem(Beatmap.Value.BeatmapInfo) { - Beatmap = { Value = Beatmap.Value.BeatmapInfo }, RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }, Client.Room?.Users.ToArray())); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 361178bfe4..57f1c31589 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -143,14 +143,12 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "test name" }, Playlist = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - RulesetID = Ruleset.Value.OnlineID, + RulesetID = Ruleset.Value.OnlineID }, - new PlaylistItem + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, RulesetID = Ruleset.Value.OnlineID, Expired = true } @@ -167,10 +165,8 @@ namespace osu.Game.Tests.Visual.Multiplayer /// /// Adds a step to create a new playlist item. /// - private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem + private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) { - Beatmap = { Value = importedBeatmap }, - BeatmapID = importedBeatmap.OnlineID, Expired = expired, PlayedAt = DateTimeOffset.Now }))); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 4040707c41..02bd93ed28 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -120,11 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add playlist item", () => { - MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem - { - Beatmap = { Value = importedBeatmap }, - BeatmapID = importedBeatmap.OnlineID, - }); + MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)); Client.AddUserPlaylistItem(userId(), item); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 7834226f15..d486a69061 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -54,9 +54,8 @@ namespace osu.Game.Tests.Visual.Multiplayer importedSet = beatmaps.GetAllUsableBeatmapSets().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); - selectedItem.Value = new PlaylistItem + selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo) { - Beatmap = { Value = Beatmap.Value.BeatmapInfo }, RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID }; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs index 44a1745eee..cc08135939 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs @@ -22,12 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo; var score = TestResources.CreateTestScoreInfo(beatmapInfo); - PlaylistItem playlistItem = new PlaylistItem - { - BeatmapID = beatmapInfo.OnlineID, - }; - - Stack.Push(screen = new MultiplayerResultsScreen(score, 1, playlistItem)); + Stack.Push(screen = new MultiplayerResultsScreen(score, 1, new PlaylistItem(beatmapInfo))); }); AddUntilStep("wait for loaded", () => screen.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 70d4d9dd55..c8077a49dc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -55,9 +55,8 @@ namespace osu.Game.Tests.Visual.Multiplayer importedSet = beatmaps.GetAllUsableBeatmapSets().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); - selectedItem.Value = new PlaylistItem + selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo) { - Beatmap = { Value = Beatmap.Value.BeatmapInfo }, RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs index dfc16c44f2..bdc348b043 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs @@ -26,18 +26,13 @@ namespace osu.Game.Tests.Visual.Multiplayer var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo; var score = TestResources.CreateTestScoreInfo(beatmapInfo); - PlaylistItem playlistItem = new PlaylistItem - { - BeatmapID = beatmapInfo.OnlineID, - }; - SortedDictionary teamScores = new SortedDictionary { { 0, new BindableInt(team1Score) }, { 1, new BindableInt(team2Score) } }; - Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, playlistItem, teamScores)); + Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, new PlaylistItem(beatmapInfo), teamScores)); }); AddUntilStep("wait for loaded", () => screen.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index 9adf2c0370..0ef8b16f68 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -107,16 +107,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); } - [Test] - public void TestChangeBeatmapAndRemove() - { - createPlaylist(); - - AddStep("change beatmap of first item", () => playlist.Items[0].BeatmapID = 30); - moveToDeleteButton(0); - AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); - } - private void moveToItem(int index, Vector2? offset = null) => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); @@ -139,25 +129,21 @@ namespace osu.Game.Tests.Visual.Multiplayer for (int i = 0; i < 20; i++) { - playlist.Items.Add(new PlaylistItem + playlist.Items.Add(new PlaylistItem(i % 2 == 1 + ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo + : new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = "Artist", + Author = new RealmUser { Username = "Creator name here" }, + Title = "Long title used to check background colour", + }, + BeatmapSet = new BeatmapSetInfo() + }) { ID = i, OwnerID = 2, - Beatmap = - { - Value = i % 2 == 1 - ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo - : new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Artist = "Artist", - Author = new RealmUser { Username = "Creator name here" }, - Title = "Long title used to check background colour", - }, - BeatmapSet = new BeatmapSetInfo() - } - }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs index 20db922122..5e4013b0f1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs @@ -31,8 +31,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { SelectedRoom.Value.Playlist.AddRange(new[] { - new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarRating = min } } }, - new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarRating = max } } }, + new PlaylistItem(new BeatmapInfo { StarRating = min }), + new PlaylistItem(new BeatmapInfo { StarRating = max }), }); }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index b66657728e..bd57c8afa5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -67,9 +67,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.TeamVersus }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -88,9 +87,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.TeamVersus }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -126,10 +124,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.HeadToHead }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -152,10 +149,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index ca3387392a..666e32d1d0 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("set name", () => SelectedRoom.Value.Name.Value = "Room name"); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set beatmap", () => SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } })); + AddStep("set beatmap", () => SelectedRoom.Value.Playlist.Add(new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo))); AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value); AddStep("clear name", () => SelectedRoom.Value.Name.Value = ""); @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Playlists { settings.NameField.Current.Value = expected_name; settings.DurationField.Current.Value = expectedDuration; - SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); + SelectedRoom.Value.Playlist.Add(new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)); RoomManager.CreateRequested = r => { @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Playlists var beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo; SelectedRoom.Value.Name.Value = "Test Room"; - SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = beatmap } }); + SelectedRoom.Value.Playlist.Add(new PlaylistItem(beatmap)); errorMessage = $"{not_found_prefix} {beatmap.OnlineID}"; @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("setup", () => { SelectedRoom.Value.Name.Value = "Test Room"; - SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); + SelectedRoom.Value.Playlist.Add(new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)); RoomManager.CreateRequested = _ => failText; }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index a05d01613c..161624413d 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -165,9 +165,8 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("load results", () => { - LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem + LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID })); }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 578ea63b4e..28b1f6eff5 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -64,9 +64,8 @@ namespace osu.Game.Tests.Visual.Playlists room.Host.Value = API.LocalUser.Value; room.RecentParticipants.Add(room.Host.Value); room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); - room.Playlist.Add(new PlaylistItem + room.Playlist.Add(new PlaylistItem(importedBeatmap.Beatmaps.First()) { - Beatmap = { Value = importedBeatmap.Beatmaps.First() }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -89,9 +88,8 @@ namespace osu.Game.Tests.Visual.Playlists room.Host.Value = API.LocalUser.Value; room.RecentParticipants.Add(room.Host.Value); room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); - room.Playlist.Add(new PlaylistItem + room.Playlist.Add(new PlaylistItem(importedBeatmap.Beatmaps.First()) { - Beatmap = { Value = importedBeatmap.Beatmaps.First() }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -106,9 +104,8 @@ namespace osu.Game.Tests.Visual.Playlists { room.Name.Value = "my awesome room"; room.Host.Value = API.LocalUser.Value; - room.Playlist.Add(new PlaylistItem + room.Playlist.Add(new PlaylistItem(importedBeatmap.Beatmaps.First()) { - Beatmap = { Value = importedBeatmap.Beatmaps.First() }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -158,21 +155,17 @@ namespace osu.Game.Tests.Visual.Playlists { room.Name.Value = "my awesome room"; room.Host.Value = API.LocalUser.Value; - room.Playlist.Add(new PlaylistItem + room.Playlist.Add(new PlaylistItem(new BeatmapInfo { - Beatmap = + MD5Hash = realHash, + OnlineID = realOnlineId, + Metadata = new BeatmapMetadata(), + BeatmapSet = new BeatmapSetInfo { - Value = new BeatmapInfo - { - MD5Hash = realHash, - OnlineID = realOnlineId, - Metadata = new BeatmapMetadata(), - BeatmapSet = new BeatmapSetInfo - { - OnlineID = realOnlineSetId, - } - } - }, + OnlineID = realOnlineSetId, + } + }) + { RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 9d45229961..a56cc7f8d6 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -727,10 +727,9 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); } - private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem + private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem(new APIBeatmap { OnlineID = item.BeatmapID }) { ID = item.ID, - BeatmapID = item.BeatmapID, OwnerID = item.OwnerID, RulesetID = item.RulesetID, Expired = item.Expired, @@ -740,14 +739,6 @@ namespace osu.Game.Online.Multiplayer AllowedMods = item.AllowedMods.ToArray() }; - /// - /// Retrieves a from an online source. - /// - /// The beatmap ID. - /// A token to cancel the request. - /// The retrieval task. - public abstract Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default); - /// /// For the provided user ID, update whether the user is included in . /// diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 3794bec228..e92bcd9769 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -9,9 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Database; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer @@ -29,9 +27,6 @@ namespace osu.Game.Online.Multiplayer private HubConnection? connection => connector?.CurrentConnection; - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - public OnlineMultiplayerClient(EndpointConfiguration endpoints) { endpoint = endpoints.MultiplayerEndpointUrl; @@ -186,11 +181,6 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } - public override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) - { - return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken); - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index d74cdd8c34..388a02f798 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -63,8 +63,8 @@ namespace osu.Game.Online.Rooms { ID = item.ID; OwnerID = item.OwnerID; - BeatmapID = item.BeatmapID; - BeatmapChecksum = item.Beatmap.Value?.MD5Hash ?? string.Empty; + BeatmapID = item.Beatmap.OnlineID; + BeatmapChecksum = item.Beatmap.MD5Hash; RulesetID = item.RulesetID; RequiredMods = item.RequiredMods.ToArray(); AllowedMods = item.AllowedMods.ToArray(); diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 90532d88b8..3620e703ae 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -64,11 +64,11 @@ namespace osu.Game.Online.Rooms downloadTracker?.RemoveAndDisposeImmediately(); - beatmapLookupCache.GetBeatmapAsync(item.NewValue.Beatmap.Value.OnlineID).ContinueWith(task => Schedule(() => + beatmapLookupCache.GetBeatmapAsync(item.NewValue.Beatmap.OnlineID).ContinueWith(task => Schedule(() => { var beatmap = task.GetResultSafely(); - if (SelectedItem.Value?.Beatmap.Value.OnlineID == beatmap.OnlineID) + if (SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID) beginTracking(beatmap); }), TaskContinuationOptions.OnlyOnRanToCompletion); }, true); @@ -96,7 +96,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap.Value), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap), (items, changes, ___) => { if (changes == null) return; diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index e78f91f20b..34c93bd9e0 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -41,6 +41,6 @@ namespace osu.Game.Online.Rooms } public static string GetTotalDuration(this BindableList playlist) => - playlist.Select(p => p.Beatmap.Value.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); + playlist.Select(p => p.Beatmap.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); } } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index c082babb01..33718f050b 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Linq; using JetBrains.Annotations; @@ -12,6 +14,7 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.Rooms { + [JsonObject(MemberSerialization.OptIn)] public class PlaylistItem : IEquatable { [JsonProperty("id")] @@ -20,9 +23,6 @@ namespace osu.Game.Online.Rooms [JsonProperty("owner_id")] public int OwnerID { get; set; } - [JsonProperty("beatmap_id")] - public int BeatmapID { get; set; } - [JsonProperty("ruleset_id")] public int RulesetID { get; set; } @@ -38,35 +38,50 @@ namespace osu.Game.Online.Rooms [JsonProperty("played_at")] public DateTimeOffset? PlayedAt { get; set; } - [JsonIgnore] - public IBindable Valid => valid; - - private readonly Bindable valid = new BindableBool(true); - - [JsonIgnore] - public readonly Bindable Beatmap = new Bindable(); - - [JsonProperty("beatmap")] - private APIBeatmap apiBeatmap { get; set; } - [JsonProperty("allowed_mods")] public APIMod[] AllowedMods { get; set; } = Array.Empty(); [JsonProperty("required_mods")] public APIMod[] RequiredMods { get; set; } = Array.Empty(); - public PlaylistItem() + /// + /// Used for deserialising from the API. + /// + [JsonProperty("beatmap")] + private APIBeatmap apiBeatmap { - Beatmap.BindValueChanged(beatmap => BeatmapID = beatmap.NewValue?.OnlineID ?? -1); + // This getter is required/used internally by JSON.NET during deserialisation to do default-value comparisons. It is never used during serialisation (see: ShouldSerializeapiBeatmap()). + // It will always return a null value on deserialisation, which JSON.NET will handle gracefully. + get => (APIBeatmap)Beatmap; + set => Beatmap = value; + } + + /// + /// Used for serialising to the API. + /// + [JsonProperty("beatmap_id")] + private int onlineBeatmapId => Beatmap.OnlineID; + + [JsonIgnore] + public IBeatmapInfo Beatmap { get; set; } = null!; + + [JsonIgnore] + public IBindable Valid => valid; + + private readonly Bindable valid = new BindableBool(true); + + [JsonConstructor] + private PlaylistItem() + { + } + + public PlaylistItem(IBeatmapInfo beatmap) + { + Beatmap = beatmap; } public void MarkInvalid() => valid.Value = false; - public void MapObjects() - { - Beatmap.Value ??= apiBeatmap; - } - #region Newtonsoft.Json implicit ShouldSerialize() methods // The properties in this region are used implicitly by Newtonsoft.Json to not serialise certain fields in some cases. @@ -82,9 +97,22 @@ namespace osu.Game.Online.Rooms #endregion - public bool Equals(PlaylistItem other) + public PlaylistItem With(IBeatmapInfo beatmap) => new PlaylistItem(beatmap) + { + ID = ID, + OwnerID = OwnerID, + RulesetID = RulesetID, + Expired = Expired, + PlaylistOrder = PlaylistOrder, + PlayedAt = PlayedAt, + AllowedMods = AllowedMods, + RequiredMods = RequiredMods, + valid = { Value = Valid.Value }, + }; + + public bool Equals(PlaylistItem? other) => ID == other?.ID - && BeatmapID == other.BeatmapID + && Beatmap.OnlineID == other.Beatmap.OnlineID && RulesetID == other.RulesetID && Expired == other.Expired && AllowedMods.SequenceEqual(other.AllowedMods) diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index e948c1adae..7cbe1a9017 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -68,14 +68,14 @@ namespace osu.Game.Screens.OnlinePlay.Components } else { - var metadataInfo = beatmap.Value.Metadata; + var metadataInfo = beatmap.Metadata; string artistUnicode = string.IsNullOrEmpty(metadataInfo.ArtistUnicode) ? metadataInfo.Artist : metadataInfo.ArtistUnicode; string titleUnicode = string.IsNullOrEmpty(metadataInfo.TitleUnicode) ? metadataInfo.Title : metadataInfo.TitleUnicode; var title = new RomanisableString($"{artistUnicode} - {titleUnicode}".Trim(), $"{metadataInfo.Artist} - {metadataInfo.Title}".Trim()); - textFlow.AddLink(title, LinkAction.OpenBeatmap, beatmap.Value.OnlineID.ToString(), "Open beatmap"); + textFlow.AddLink(title, LinkAction.OpenBeatmap, beatmap.OnlineID.ToString(), "Open beatmap"); } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs index d534a1e374..8402619ebc 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Components var mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray(); drawableRuleset.FadeIn(transition_duration); - drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, ruleset.RulesetInfo, mods) { Size = new Vector2(height) }; + drawableRuleset.Child = new DifficultyIcon(item.Beatmap, ruleset.RulesetInfo, mods) { Size = new Vector2(height) }; } else drawableRuleset.FadeOut(transition_duration); diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs index ffc5c07d4e..8906bebf0e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { Schedule(() => { - var beatmap = playlistItem?.Beatmap.Value; + var beatmap = playlistItem?.Beatmap; string? lastCover = (background?.Beatmap?.BeatmapSet as IBeatmapSetOnlineInfo)?.Covers.Cover; string? newCover = (beatmap?.BeatmapSet as IBeatmapSetOnlineInfo)?.Covers.Cover; diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs index d144e1e3a9..d46ff12279 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateBeatmap() { - sprite.Beatmap.Value = Playlist.GetCurrentItem()?.Beatmap.Value; + sprite.Beatmap.Value = Playlist.GetCurrentItem()?.Beatmap; } protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs b/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs index f3e90aa396..7e31591389 100644 --- a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs +++ b/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Components public PlaylistItemBackground(PlaylistItem? playlistItem) { - Beatmap = playlistItem?.Beatmap.Value; + Beatmap = playlistItem?.Beatmap; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 21b64b61bb..4242886e66 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -112,9 +112,6 @@ namespace osu.Game.Screens.OnlinePlay.Components try { - foreach (var pi in room.Playlist) - pi.MapObjects(); - var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); if (existing == null) rooms.Add(room); diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index edf9c5d155..95ecadd21a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateRange(object sender, NotifyCollectionChangedEventArgs e) { - var orderedDifficulties = Playlist.Where(p => p.Beatmap.Value != null).Select(p => p.Beatmap.Value).OrderBy(b => b.StarRating).ToArray(); + var orderedDifficulties = Playlist.Select(p => p.Beatmap).OrderBy(b => b.StarRating).ToArray(); StarDifficulty minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0); StarDifficulty maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarRating : 0, 0); diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 7674fac88e..4be1927e57 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay Schedule(() => ownerAvatar.User = foundUser); } - beatmap = await beatmapLookupCache.GetBeatmapAsync(Item.Beatmap.Value.OnlineID).ConfigureAwait(false); + beatmap = await beatmapLookupCache.GetBeatmapAsync(Item.Beatmap.OnlineID).ConfigureAwait(false); Scheduler.AddOnce(refresh); } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 60f65fbd9f..a1a82c907a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -409,7 +409,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return; var cancellationSource = beatmapLookupCancellation = new CancellationTokenSource(); - beatmapLookupCache.GetBeatmapAsync(beatmap.Value.OnlineID, cancellationSource.Token) + beatmapLookupCache.GetBeatmapAsync(beatmap.OnlineID, cancellationSource.Token) .ContinueWith(task => Schedule(() => { if (cancellationSource.IsCancellationRequested) diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index a7b907c7d2..cdd2ae0c9c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (editButton != null) host.BindValueChanged(h => editButton.Alpha = h.NewValue?.Equals(api.LocalUser.Value) == true ? 1 : 0, true); - SelectedItem.BindValueChanged(item => background.Beatmap.Value = item.NewValue?.Beatmap.Value, true); + SelectedItem.BindValueChanged(item => background.Beatmap.Value = item.NewValue?.Beatmap, true); } protected override Drawable CreateBackground() => background = new BackgroundSprite(); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 836629ada0..e297c90491 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -376,7 +376,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private void updateWorkingBeatmap() { - var beatmap = SelectedItem.Value?.Beatmap.Value; + var beatmap = SelectedItem.Value?.Beatmap; // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 12caf1fde1..e30ec36e9c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -67,8 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer var multiplayerItem = new MultiplayerPlaylistItem { ID = itemToEdit ?? 0, - BeatmapID = item.BeatmapID, - BeatmapChecksum = item.Beatmap.Value.MD5Hash, + BeatmapID = item.Beatmap.OnlineID, + BeatmapChecksum = item.Beatmap.MD5Hash, RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.ToArray(), AllowedMods = item.AllowedMods.ToArray() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index cb50a56052..429b0ad89b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -398,38 +397,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void updateCurrentItem() { Debug.Assert(client.Room != null); - - var expectedSelectedItem = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); - - if (expectedSelectedItem == null) - return; - - // There's no reason to renew the selected item if its content hasn't changed. - if (SelectedItem.Value?.Equals(expectedSelectedItem) == true && expectedSelectedItem.Beatmap.Value != null) - return; - - // Clear the selected item while the lookup is performed, so components like the ready button can enter their disabled states. - SelectedItem.Value = null; - - if (expectedSelectedItem.Beatmap.Value == null) - { - Task.Run(async () => - { - var beatmap = await client.GetAPIBeatmap(expectedSelectedItem.BeatmapID).ConfigureAwait(false); - - Schedule(() => - { - expectedSelectedItem.Beatmap.Value = beatmap; - - if (Room.Playlist.SingleOrDefault(i => i.ID == client.Room?.Settings.PlaylistItemId)?.Equals(expectedSelectedItem) == true) - applyCurrentItem(); - }); - }); - } - else - applyCurrentItem(); - - void applyCurrentItem() => SelectedItem.Value = expectedSelectedItem; + SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); } private void handleRoomLost() => Schedule(() => diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index eab1f83967..7b64784316 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -113,12 +113,8 @@ namespace osu.Game.Screens.OnlinePlay { itemSelected = true; - var item = new PlaylistItem + var item = new PlaylistItem(Beatmap.Value.BeatmapInfo) { - Beatmap = - { - Value = Beatmap.Value.BeatmapInfo - }, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 2b071175d5..8403e1e0fe 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void load(IBindable ruleset) { // Sanity checks to ensure that PlaylistsPlayer matches the settings for the current PlaylistItem - if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(PlaylistItem.Beatmap.Value)) + if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(PlaylistItem.Beatmap)) throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap"); if (ruleset.Value.OnlineID != PlaylistItem.RulesetID) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 6c8ab52d22..6674a37c3c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -392,7 +392,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists foreach (var item in Playlist) { - if (invalidBeatmapIDs.Contains(item.BeatmapID)) + if (invalidBeatmapIDs.Contains(item.Beatmap.OnlineID)) item.MarkInvalid(); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 3ac576b18e..86591c1d6d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -41,10 +41,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void createNewItem() { - PlaylistItem item = new PlaylistItem + PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo) { ID = Playlist.Count == 0 ? 0 : Playlist.Max(p => p.ID) + 1, - Beatmap = { Value = Beatmap.Value.BeatmapInfo }, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index a9b3ca4991..2fecf0a4cc 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -46,9 +46,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "test name" }, Playlist = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, RulesetID = Ruleset.Value.OnlineID } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 15ede6cc26..6dc5159b6f 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -7,14 +7,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Game.Beatmaps; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; @@ -39,9 +36,6 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved] - private BeatmapManager beatmaps { get; set; } = null!; - private readonly TestMultiplayerRoomManager roomManager; /// @@ -407,23 +401,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, playlistItemId); - public override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) - { - IBeatmapInfo? beatmap = roomManager.ServerSideRooms.SelectMany(r => r.Playlist) - .FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value - ?? beatmaps.QueryBeatmap(b => b.OnlineID == beatmapId); - - if (beatmap == null) - throw new InvalidOperationException("Beatmap not found."); - - return Task.FromResult(new APIBeatmap - { - BeatmapSet = new APIBeatmapSet { OnlineID = beatmap.BeatmapSet?.OnlineID ?? -1 }, - OnlineID = beatmapId, - Checksum = beatmap.MD5Hash - }); - } - private async Task changeMatchType(MatchType type) { Debug.Assert(Room != null); diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index 73d0df2c36..8dfd969c51 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -43,16 +43,9 @@ namespace osu.Game.Tests.Visual.OnlinePlay if (ruleset != null) { - room.Playlist.Add(new PlaylistItem + room.Playlist.Add(new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID, - Beatmap = - { - Value = new BeatmapInfo - { - Metadata = new BeatmapMetadata() - } - } }); } From 48573d2401d33434399f9abaf98659ac2776fa81 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:42:37 +0900 Subject: [PATCH 560/587] Move test request handling earlier in setup --- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 448ec5e3d9..b6a347a896 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -60,24 +60,16 @@ namespace osu.Game.Tests.Visual.OnlinePlay drawableDependenciesContainer.Clear(); dependencies.OnlinePlayDependencies = CreateOnlinePlayDependencies(); drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents); + + var handler = OnlinePlayDependencies.RequestsHandler; + + // Resolving the BeatmapManager in the test scene will inject the game-wide BeatmapManager, while many test scenes cache their own BeatmapManager instead. + // To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead. + var beatmapManager = dependencies.Get(); + + ((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, beatmapManager); }); - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("setup API", () => - { - var handler = OnlinePlayDependencies.RequestsHandler; - - // Resolving the BeatmapManager in the test scene will inject the game-wide BeatmapManager, while many test scenes cache their own BeatmapManager instead. - // To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead. - var beatmapManager = dependencies.Get(); - - ((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, beatmapManager); - }); - } - /// /// Creates the room dependencies. Called every . /// From b1dbd4abfe6697134717439d1fbe06eb91c94cf5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:50:07 +0900 Subject: [PATCH 561/587] Fix incorrect playlist item <-> availability tracker logic Results in revert to some prior logic for the tracker implementation. --- .../OnlinePlayBeatmapAvailabilityTracker.cs | 19 +++++++++---------- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 5 +---- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 3620e703ae..02bcd1f30c 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -96,7 +96,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => filteredBeatmaps(beatmap), (items, changes, ___) => { if (changes == null) return; @@ -125,7 +125,7 @@ namespace osu.Game.Online.Rooms break; case DownloadState.LocallyAvailable: - bool available = QueryBeatmapForOnlinePlay(realm.Realm, beatmap).Any(); + bool available = filteredBeatmaps(beatmap).Any(); availability.Value = available ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); @@ -140,15 +140,14 @@ namespace osu.Game.Online.Rooms } } - /// - /// Performs a query for a local matching a requested one for the purpose of online play. - /// - /// The realm to query from. - /// The requested beatmap. - /// A beatmap query. - public static IQueryable QueryBeatmapForOnlinePlay(Realm realm, IBeatmapInfo beatmap) + private IQueryable filteredBeatmaps(APIBeatmap beatmap) { - return realm.All().Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", beatmap.OnlineID, beatmap.MD5Hash); + int onlineId = beatmap.OnlineID; + string checksum = beatmap.MD5Hash; + + return realm.Realm + .All() + .Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 4be1927e57..faeb73cfa3 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -97,9 +97,6 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } - [Resolved] - private RealmAccess realm { get; set; } - protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model; public DrawableRoomPlaylistItem(PlaylistItem item) @@ -444,7 +441,7 @@ namespace osu.Game.Screens.OnlinePlay Alpha = AllowShowingResults ? 1 : 0, TooltipText = "View results" }, - OnlinePlayBeatmapAvailabilityTracker.QueryBeatmapForOnlinePlay(realm.Realm, beatmap).Any() ? Empty() : new PlaylistDownloadButton(beatmap), + beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap), editButton = new PlaylistEditButton { Size = new Vector2(30, 30), From f5b34e03139a7d098fe30e44c9e6fcad3cd890ca Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:51:44 +0900 Subject: [PATCH 562/587] Fix some test failures due to now-async lookups --- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 4 ++-- .../Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index bea15e6e58..4f01c14659 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -168,7 +168,7 @@ namespace osu.Game.Tests.Visual.Multiplayer assertDownloadButtonVisible(false); void assertDownloadButtonVisible(bool visible) => AddUntilStep($"download button {(visible ? "shown" : "hidden")}", - () => playlist.ChildrenOfType().Single().Alpha == (visible ? 1 : 0)); + () => playlist.ChildrenOfType().SingleOrDefault()?.Alpha == (visible ? 1 : 0)); } [Test] @@ -260,7 +260,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } private void moveToItem(int index, Vector2? offset = null) - => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); + => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); private void moveToDragger(int index, Vector2? offset = null) => AddStep($"move mouse to dragger {index}", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index 0ef8b16f68..98dc243ab5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Containers; using osu.Game.Models; using osu.Game.Online.API; @@ -108,7 +107,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } private void moveToItem(int index, Vector2? offset = null) - => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); + => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () => { From 6e99fe04c3b71ce5a36609062ceb08bd6fb1e400 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 00:39:43 +0900 Subject: [PATCH 563/587] Revert more NUnit test adapter bumps --- .../osu.Game.Rulesets.EmptyFreeform.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- .../osu.Game.Rulesets.EmptyScrolling.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index c305872288..cb922c5a58 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index ec90885cd9..5ecd9cc675 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index f2e143a9c5..33ad0ac4f7 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index ec90885cd9..5ecd9cc675 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + From 6a08fd57efb0012a723d658bc279d88292f3754e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 09:43:28 +0900 Subject: [PATCH 564/587] Rename "client" fields in tests to specify whether spectator or multiplayer --- .../StatefulMultiplayerClientTest.cs | 28 +-- .../Visual/Gameplay/TestSceneSpectator.cs | 6 +- .../Visual/Multiplayer/QueueModeTestScene.cs | 10 +- .../TestSceneAllPlayersQueueMode.cs | 38 ++-- .../TestSceneCreateMultiplayerMatchButton.cs | 4 +- .../Multiplayer/TestSceneHostOnlyQueueMode.cs | 26 +-- .../TestSceneMultiSpectatorLeaderboard.cs | 2 +- .../TestSceneMultiSpectatorScreen.cs | 12 +- .../Multiplayer/TestSceneMultiplayer.cs | 180 +++++++++--------- ...TestSceneMultiplayerGameplayLeaderboard.cs | 6 +- ...ceneMultiplayerGameplayLeaderboardTeams.cs | 4 +- .../TestSceneMultiplayerMatchSubScreen.cs | 10 +- .../TestSceneMultiplayerParticipantsList.cs | 98 +++++----- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 6 +- .../TestSceneMultiplayerPlaylist.cs | 12 +- .../TestSceneMultiplayerQueueList.cs | 28 +-- .../TestSceneMultiplayerReadyButton.cs | 56 +++--- .../TestSceneMultiplayerSpectateButton.cs | 26 +-- .../Multiplayer/TestSceneRankRangePill.cs | 18 +- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 30 +-- .../Visual/TestMultiplayerComponents.cs | 4 +- .../IMultiplayerTestSceneDependencies.cs | 5 +- .../Multiplayer/MultiplayerTestScene.cs | 4 +- .../MultiplayerTestSceneDependencies.cs | 6 +- 24 files changed, 309 insertions(+), 310 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 0c49a18c8f..4adb7002a0 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -21,8 +21,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer { var user = new APIUser { Id = 33 }; - AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + AddRepeatStep("add user multiple times", () => MultiplayerClient.AddUser(user), 3); + AddAssert("room has 2 users", () => MultiplayerClient.Room?.Users.Count == 2); } [Test] @@ -30,11 +30,11 @@ namespace osu.Game.Tests.NonVisual.Multiplayer { var user = new APIUser { Id = 44 }; - AddStep("add user", () => Client.AddUser(user)); - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + AddStep("add user", () => MultiplayerClient.AddUser(user)); + AddAssert("room has 2 users", () => MultiplayerClient.Room?.Users.Count == 2); - AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); - AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); + AddRepeatStep("remove user multiple times", () => MultiplayerClient.RemoveUser(user), 3); + AddAssert("room has 1 user", () => MultiplayerClient.Room?.Users.Count == 1); } [Test] @@ -42,7 +42,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer { int id = 2000; - AddRepeatStep("add some users", () => Client.AddUser(new APIUser { Id = id++ }), 5); + AddRepeatStep("add some users", () => MultiplayerClient.AddUser(new APIUser { Id = id++ }), 5); checkPlayingUserCount(0); changeState(3, MultiplayerUserState.WaitingForLoad); @@ -57,17 +57,17 @@ namespace osu.Game.Tests.NonVisual.Multiplayer changeState(6, MultiplayerUserState.WaitingForLoad); checkPlayingUserCount(6); - AddStep("another user left", () => Client.RemoveUser((Client.Room?.Users.Last().User).AsNonNull())); + AddStep("another user left", () => MultiplayerClient.RemoveUser((MultiplayerClient.Room?.Users.Last().User).AsNonNull())); checkPlayingUserCount(5); - AddStep("leave room", () => Client.LeaveRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); checkPlayingUserCount(0); } [Test] public void TestPlayingUsersUpdatedOnJoin() { - AddStep("leave room", () => Client.LeaveRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); AddStep("create room initially in gameplay", () => @@ -76,7 +76,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer newRoom.CopyFrom(SelectedRoom.Value); newRoom.RoomID.Value = null; - Client.RoomSetupAction = room => + MultiplayerClient.RoomSetupAction = room => { room.State = MultiplayerRoomState.Playing; room.Users.Add(new MultiplayerRoomUser(PLAYER_1_ID) @@ -94,15 +94,15 @@ namespace osu.Game.Tests.NonVisual.Multiplayer } private void checkPlayingUserCount(int expectedCount) - => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => Client.CurrentMatchPlayingUserIds.Count == expectedCount); + => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count == expectedCount); private void changeState(int userCount, MultiplayerUserState state) => AddStep($"{"user".ToQuantity(userCount)} in {state}", () => { for (int i = 0; i < userCount; ++i) { - int userId = Client.Room?.Users[i].UserID ?? throw new AssertionException("Room cannot be null!"); - Client.ChangeUserState(userId, state); + int userId = MultiplayerClient.Room?.Users[i].UserID ?? throw new AssertionException("Room cannot be null!"); + MultiplayerClient.ChangeUserState(userId, state); } }); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 6dca256d31..d47ebf9f0d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuGameBase game { get; set; } - private TestSpectatorClient spectatorClient => dependenciesScreen.Client; + private TestSpectatorClient spectatorClient => dependenciesScreen.SpectatorClient; private DependenciesScreen dependenciesScreen; private SoloSpectator spectatorScreen; @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay Children = new Drawable[] { dependenciesScreen.UserLookupCache, - dependenciesScreen.Client, + dependenciesScreen.SpectatorClient, }; }); @@ -336,7 +336,7 @@ namespace osu.Game.Tests.Visual.Gameplay private class DependenciesScreen : OsuScreen { [Cached(typeof(SpectatorClient))] - public readonly TestSpectatorClient Client = new TestSpectatorClient(); + public readonly TestSpectatorClient SpectatorClient = new TestSpectatorClient(); [Cached(typeof(UserLookupCache))] public readonly TestUserLookupCache UserLookupCache = new TestUserLookupCache(); diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 52801dd57a..2a3b44d619 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerComponents multiplayerComponents; - protected TestMultiplayerClient Client => multiplayerComponents.Client; + protected TestMultiplayerClient MultiplayerClient => multiplayerComponents.MultiplayerClient; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -84,21 +84,21 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); - AddUntilStep("wait for join", () => Client.RoomJoined); + AddUntilStep("wait for join", () => MultiplayerClient.RoomJoined); } [Test] public void TestCreatedWithCorrectMode() { - AddAssert("room created with correct mode", () => Client.APIRoom?.QueueMode.Value == Mode); + AddAssert("room created with correct mode", () => MultiplayerClient.APIRoom?.QueueMode.Value == Mode); } protected void RunGameplay() { - AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle); + AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); ClickButtonWhenEnabled(); - AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); + AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index ad60ac824d..0785315b26 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -31,19 +31,19 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestFirstItemSelectedByDefault() { - AddAssert("first item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("first item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] public void TestItemAddedToTheEndOfQueue() { addItem(() => OtherBeatmap); - AddAssert("playlist has 2 items", () => Client.APIRoom?.Playlist.Count == 2); + AddAssert("playlist has 2 items", () => MultiplayerClient.APIRoom?.Playlist.Count == 2); addItem(() => InitialBeatmap); - AddAssert("playlist has 3 items", () => Client.APIRoom?.Playlist.Count == 3); + AddAssert("playlist has 3 items", () => MultiplayerClient.APIRoom?.Playlist.Count == 3); - AddAssert("first item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("first item still selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] @@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { RunGameplay(); - AddAssert("playlist has only one item", () => Client.APIRoom?.Playlist.Count == 1); - AddAssert("playlist item is expired", () => Client.APIRoom?.Playlist[0].Expired == true); - AddAssert("last item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("playlist has only one item", () => MultiplayerClient.APIRoom?.Playlist.Count == 1); + AddAssert("playlist item is expired", () => MultiplayerClient.APIRoom?.Playlist[0].Expired == true); + AddAssert("last item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] @@ -64,13 +64,13 @@ namespace osu.Game.Tests.Visual.Multiplayer RunGameplay(); - AddAssert("first item expired", () => Client.APIRoom?.Playlist[0].Expired == true); - AddAssert("next item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[1].ID); + AddAssert("first item expired", () => MultiplayerClient.APIRoom?.Playlist[0].Expired == true); + AddAssert("next item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[1].ID); RunGameplay(); - AddAssert("second item expired", () => Client.APIRoom?.Playlist[1].Expired == true); - AddAssert("next item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[2].ID); + AddAssert("second item expired", () => MultiplayerClient.APIRoom?.Playlist[1].Expired == true); + AddAssert("next item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[2].ID); } [Test] @@ -82,10 +82,10 @@ namespace osu.Game.Tests.Visual.Multiplayer // Move to the "other" beatmap. RunGameplay(); - AddStep("change queue mode", () => Client.ChangeSettings(queueMode: QueueMode.HostOnly)); - AddAssert("playlist has 3 items", () => Client.APIRoom?.Playlist.Count == 3); - AddAssert("item 2 is not expired", () => Client.APIRoom?.Playlist[1].Expired == false); - AddAssert("current item is the other beatmap", () => Client.Room?.Settings.PlaylistItemId == 2); + AddStep("change queue mode", () => MultiplayerClient.ChangeSettings(queueMode: QueueMode.HostOnly)); + AddAssert("playlist has 3 items", () => MultiplayerClient.APIRoom?.Playlist.Count == 3); + AddAssert("item 2 is not expired", () => MultiplayerClient.APIRoom?.Playlist[1].Expired == false); + AddAssert("current item is the other beatmap", () => MultiplayerClient.Room?.Settings.PlaylistItemId == 2); } [Test] @@ -101,10 +101,10 @@ namespace osu.Game.Tests.Visual.Multiplayer addItem(() => OtherBeatmap, new CatchRuleset().RulesetInfo); AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); - AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle); + AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); ClickButtonWhenEnabled(); - AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); + AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); @@ -118,10 +118,10 @@ namespace osu.Game.Tests.Visual.Multiplayer addItem(() => OtherBeatmap, mods: new Mod[] { new OsuModDoubleTime() }); AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); - AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle); + AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); ClickButtonWhenEnabled(); - AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); + AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs index 2f0398c6ef..0674fc7a39 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs @@ -37,10 +37,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("end joining room", () => joiningRoomOperation.Dispose()); assertButtonEnableState(true); - AddStep("disconnect client", () => Client.Disconnect()); + AddStep("disconnect client", () => MultiplayerClient.Disconnect()); assertButtonEnableState(false); - AddStep("re-connect client", () => Client.Connect()); + AddStep("re-connect client", () => MultiplayerClient.Connect()); assertButtonEnableState(true); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index cd14a98751..a308d60cf1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestFirstItemSelectedByDefault() { - AddAssert("first item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("first item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { selectNewItem(() => InitialBeatmap); - AddAssert("playlist item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("playlist item still selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { selectNewItem(() => OtherBeatmap); - AddAssert("playlist item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("playlist item still selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] @@ -46,10 +46,10 @@ namespace osu.Game.Tests.Visual.Multiplayer { RunGameplay(); - AddAssert("playlist contains two items", () => Client.APIRoom?.Playlist.Count == 2); - AddAssert("first playlist item expired", () => Client.APIRoom?.Playlist[0].Expired == true); - AddAssert("second playlist item not expired", () => Client.APIRoom?.Playlist[1].Expired == false); - AddAssert("second playlist item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[1].ID); + AddAssert("playlist contains two items", () => MultiplayerClient.APIRoom?.Playlist.Count == 2); + AddAssert("first playlist item expired", () => MultiplayerClient.APIRoom?.Playlist[0].Expired == true); + AddAssert("second playlist item not expired", () => MultiplayerClient.APIRoom?.Playlist[1].Expired == false); + AddAssert("second playlist item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[1].ID); } [Test] @@ -58,23 +58,23 @@ namespace osu.Game.Tests.Visual.Multiplayer RunGameplay(); IBeatmapInfo firstBeatmap = null; - AddStep("get first playlist item beatmap", () => firstBeatmap = Client.APIRoom?.Playlist[0].Beatmap.Value); + AddStep("get first playlist item beatmap", () => firstBeatmap = MultiplayerClient.APIRoom?.Playlist[0].Beatmap.Value); selectNewItem(() => OtherBeatmap); - AddAssert("first playlist item hasn't changed", () => Client.APIRoom?.Playlist[0].Beatmap.Value == firstBeatmap); - AddAssert("second playlist item changed", () => Client.APIRoom?.Playlist[1].Beatmap.Value != firstBeatmap); + AddAssert("first playlist item hasn't changed", () => MultiplayerClient.APIRoom?.Playlist[0].Beatmap.Value == firstBeatmap); + AddAssert("second playlist item changed", () => MultiplayerClient.APIRoom?.Playlist[1].Beatmap.Value != firstBeatmap); } [Test] public void TestSettingsUpdatedWhenChangingQueueMode() { - AddStep("change queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings + AddStep("change queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); - AddUntilStep("api room updated", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + AddUntilStep("api room updated", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); } [Test] @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { addItem(() => OtherBeatmap); - AddAssert("playlist contains two items", () => Client.APIRoom?.Playlist.Count == 2); + AddAssert("playlist contains two items", () => MultiplayerClient.APIRoom?.Playlist.Count == 2); } private void selectNewItem(Func beatmap) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 543e6a91d0..6b3573b3cb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach ((int userId, var _) in clocks) { SpectatorClient.StartPlay(userId, 0); - OnlinePlayDependencies.Client.AddUser(new APIUser { Id = userId }); + OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = userId }); } }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 56cb6036c7..7ce0c6a94d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -60,8 +60,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("start players silently", () => { - OnlinePlayDependencies.Client.AddUser(new APIUser { Id = PLAYER_1_ID }, true); - OnlinePlayDependencies.Client.AddUser(new APIUser { Id = PLAYER_2_ID }, true); + OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }, true); + OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = PLAYER_2_ID }, true); playingUsers.Add(new MultiplayerRoomUser(PLAYER_1_ID)); playingUsers.Add(new MultiplayerRoomUser(PLAYER_2_ID)); @@ -121,13 +121,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("start players", () => { - var player1 = OnlinePlayDependencies.Client.AddUser(new APIUser { Id = PLAYER_1_ID }, true); + var player1 = OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }, true); player1.MatchState = new TeamVersusUserState { TeamID = 0, }; - var player2 = OnlinePlayDependencies.Client.AddUser(new APIUser { Id = PLAYER_2_ID }, true); + var player2 = OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = PLAYER_2_ID }, true); player2.MatchState = new TeamVersusUserState { TeamID = 1, @@ -396,7 +396,7 @@ namespace osu.Game.Tests.Visual.Multiplayer User = new APIUser { Id = id }, }; - OnlinePlayDependencies.Client.AddUser(user.User, true); + OnlinePlayDependencies.MultiplayerClient.AddUser(user.User, true); SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId); playingUsers.Add(user); @@ -410,7 +410,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { var user = playingUsers.Single(u => u.UserID == userId); - OnlinePlayDependencies.Client.RemoveUser(user.User.AsNonNull()); + OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull()); SpectatorClient.EndPlay(userId); playingUsers.Remove(user); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 5439b3eeea..3f9aec3a42 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerComponents multiplayerComponents; - private TestMultiplayerClient client => multiplayerComponents.Client; + private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient; private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager; [BackgroundDependencyLoader] @@ -109,66 +109,66 @@ namespace osu.Game.Tests.Visual.Multiplayer // all ready AddUntilStep("all players ready", () => { - var nextUnready = client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); + var nextUnready = multiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); if (nextUnready != null) - client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); + multiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); - return client.Room?.Users.All(u => u.State == MultiplayerUserState.Ready) == true; + return multiplayerClient.Room?.Users.All(u => u.State == MultiplayerUserState.Ready) == true; }); AddStep("unready all players at once", () => { - Debug.Assert(client.Room != null); + Debug.Assert(multiplayerClient.Room != null); - foreach (var u in client.Room.Users) client.ChangeUserState(u.UserID, MultiplayerUserState.Idle); + foreach (var u in multiplayerClient.Room.Users) multiplayerClient.ChangeUserState(u.UserID, MultiplayerUserState.Idle); }); AddStep("ready all players at once", () => { - Debug.Assert(client.Room != null); + Debug.Assert(multiplayerClient.Room != null); - foreach (var u in client.Room.Users) client.ChangeUserState(u.UserID, MultiplayerUserState.Ready); + foreach (var u in multiplayerClient.Room.Users) multiplayerClient.ChangeUserState(u.UserID, MultiplayerUserState.Ready); }); } private void addRandomPlayer() { int randomUser = RNG.Next(200000, 500000); - client.AddUser(new APIUser { Id = randomUser, Username = $"user {randomUser}" }); + multiplayerClient.AddUser(new APIUser { Id = randomUser, Username = $"user {randomUser}" }); } private void removeLastUser() { - APIUser lastUser = client.Room?.Users.Last().User; + APIUser lastUser = multiplayerClient.Room?.Users.Last().User; - if (lastUser == null || lastUser == client.LocalUser?.User) + if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User) return; - client.RemoveUser(lastUser); + multiplayerClient.RemoveUser(lastUser); } private void kickLastUser() { - APIUser lastUser = client.Room?.Users.Last().User; + APIUser lastUser = multiplayerClient.Room?.Users.Last().User; - if (lastUser == null || lastUser == client.LocalUser?.User) + if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User) return; - client.KickUser(lastUser.Id); + multiplayerClient.KickUser(lastUser.Id); } private void markNextPlayerReady() { - var nextUnready = client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); + var nextUnready = multiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); if (nextUnready != null) - client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); + multiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); } private void markNextPlayerIdle() { - var nextUnready = client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Ready); + var nextUnready = multiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Ready); if (nextUnready != null) - client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Idle); + multiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Idle); } private void performRandomAction() @@ -218,7 +218,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Press select", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); } [Test] @@ -237,8 +237,8 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddAssert("Check participant count correct", () => client.APIRoom?.ParticipantCount.Value == 1); - AddAssert("Check participant list contains user", () => client.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1); + AddAssert("Check participant count correct", () => multiplayerClient.APIRoom?.ParticipantCount.Value == 1); + AddAssert("Check participant list contains user", () => multiplayerClient.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1); } [Test] @@ -297,10 +297,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); - AddAssert("Check participant count correct", () => client.APIRoom?.ParticipantCount.Value == 1); - AddAssert("Check participant list contains user", () => client.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1); + AddAssert("Check participant count correct", () => multiplayerClient.APIRoom?.ParticipantCount.Value == 1); + AddAssert("Check participant list contains user", () => multiplayerClient.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1); } [Test] @@ -320,7 +320,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddAssert("room has password", () => client.APIRoom?.Password.Value == "password"); + AddAssert("room has password", () => multiplayerClient.APIRoom?.Password.Value == "password"); } [Test] @@ -355,7 +355,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); } [Test] @@ -375,8 +375,8 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("change password", () => client.ChangeSettings(password: "password2")); - AddUntilStep("local password changed", () => client.APIRoom?.Password.Value == "password2"); + AddStep("change password", () => multiplayerClient.ChangeSettings(password: "password2")); + AddUntilStep("local password changed", () => multiplayerClient.APIRoom?.Password.Value == "password2"); } [Test] @@ -398,7 +398,7 @@ namespace osu.Game.Tests.Visual.Multiplayer pressReadyButton(); AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); - AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); + AddUntilStep("user state is idle", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); } [Test] @@ -422,22 +422,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; - ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId); + ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.Room?.Settings.PlaylistItemId); }); AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); - AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); + AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == multiplayerClient.Room?.Playlist.First().BeatmapID); AddStep("Select next beatmap", () => InputManager.Key(Key.Down)); - AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != client.Room?.Playlist.First().BeatmapID); + AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID); - AddStep("start match externally", () => client.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); - AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); + AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == multiplayerClient.Room?.Playlist.First().BeatmapID); } [Test] @@ -461,22 +461,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; - ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId); + ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.Room?.Settings.PlaylistItemId); }); AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); - AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == client.Room?.Playlist.First().RulesetID); + AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == multiplayerClient.Room?.Playlist.First().RulesetID); AddStep("Switch ruleset", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Ruleset.Value = new CatchRuleset().RulesetInfo); - AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != client.Room?.Playlist.First().RulesetID); + AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID); - AddStep("start match externally", () => client.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); - AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == client.Room?.Playlist.First().RulesetID); + AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == multiplayerClient.Room?.Playlist.First().RulesetID); } [Test] @@ -500,22 +500,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; - ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId); + ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.Room?.Settings.PlaylistItemId); }); AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); - AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(client.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); + AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() }); - AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(client.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); + AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); - AddStep("start match externally", () => client.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); - AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(client.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); + AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); } [Test] @@ -536,18 +536,18 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user (ready, host)", () => { - client.AddUser(new APIUser { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" }); - client.TransferHost(MultiplayerTestScene.PLAYER_1_ID); - client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); + multiplayerClient.AddUser(new APIUser { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" }); + multiplayerClient.TransferHost(MultiplayerTestScene.PLAYER_1_ID); + multiplayerClient.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); }); AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); ClickButtonWhenEnabled(); - AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("start match externally", () => client.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch()); AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen()); } @@ -572,16 +572,16 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user (ready, host)", () => { - client.AddUser(new APIUser { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" }); - client.TransferHost(MultiplayerTestScene.PLAYER_1_ID); - client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); + multiplayerClient.AddUser(new APIUser { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" }); + multiplayerClient.TransferHost(MultiplayerTestScene.PLAYER_1_ID); + multiplayerClient.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); }); ClickButtonWhenEnabled(); - AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("start match externally", () => client.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch()); AddStep("restore beatmap", () => { @@ -608,7 +608,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("disconnect", () => client.Disconnect()); + AddStep("disconnect", () => multiplayerClient.Disconnect()); AddUntilStep("back in lounge", () => this.ChildrenOfType().FirstOrDefault()?.IsCurrentScreen() == true); } @@ -718,7 +718,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); AddAssert("local room has correct settings", () => { @@ -745,11 +745,11 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); - AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddStep("set spectating state", () => multiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("state set to spectating", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready)); + AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); + AddStep("set other user ready", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready)); pressReadyButton(1234); AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); @@ -761,7 +761,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen()); - AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); + AddUntilStep("user state is idle", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); } [Test] @@ -781,16 +781,16 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); - AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddStep("set spectating state", () => multiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("state set to spectating", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready)); + AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); + AddStep("set other user ready", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready)); pressReadyButton(1234); AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); - AddStep("set other user loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); - AddStep("set other user finished play", () => client.ChangeUserState(1234, MultiplayerUserState.FinishedPlay)); + AddStep("set other user loaded", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Loaded)); + AddStep("set other user finished play", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.FinishedPlay)); AddStep("press back button and exit", () => { @@ -800,7 +800,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen()); AddWaitStep("wait for possible state change", 5); - AddUntilStep("user state is spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddUntilStep("user state is spectating", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); } [Test] @@ -822,13 +822,13 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); - AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem + AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); + AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID })).WaitSafely()); - AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); + AddUntilStep("item arrived in playlist", () => multiplayerClient.Room?.Playlist.Count == 2); AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); AddUntilStep("queue contains item", () => this.ChildrenOfType().Single().Items.Single().ID == 2); @@ -853,16 +853,16 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); - AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem + AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); + AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID })).WaitSafely()); - AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); + AddUntilStep("item arrived in playlist", () => multiplayerClient.Room?.Playlist.Count == 2); - AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2).WaitSafely()); - AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1); + AddStep("delete item as other user", () => multiplayerClient.RemoveUserPlaylistItem(1234, 2).WaitSafely()); + AddUntilStep("item removed from playlist", () => multiplayerClient.Room?.Playlist.Count == 1); AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0); @@ -886,17 +886,17 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user and make host", () => { - client.AddUser(new APIUser { Id = 1234 }); - client.TransferHost(1234); + multiplayerClient.AddUser(new APIUser { Id = 1234 }); + multiplayerClient.TransferHost(1234); }); - AddStep("set local user spectating", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); - AddUntilStep("wait for spectating state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddStep("set local user spectating", () => multiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("wait for spectating state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); runGameplay(); - AddStep("exit gameplay for other user", () => client.ChangeUserState(1234, MultiplayerUserState.Idle)); - AddUntilStep("wait for room to be idle", () => client.Room?.State == MultiplayerRoomState.Open); + AddStep("exit gameplay for other user", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Idle)); + AddUntilStep("wait for room to be idle", () => multiplayerClient.Room?.State == MultiplayerRoomState.Open); runGameplay(); @@ -904,13 +904,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("start match by other user", () => { - client.ChangeUserState(1234, MultiplayerUserState.Ready); - client.StartMatch(); + multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready); + multiplayerClient.StartMatch(); }); - AddUntilStep("wait for loading", () => client.Room?.State == MultiplayerRoomState.WaitingForLoad); - AddStep("set player loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); - AddUntilStep("wait for gameplay to start", () => client.Room?.State == MultiplayerRoomState.Playing); + AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); + AddStep("set player loaded", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Loaded)); + AddUntilStep("wait for gameplay to start", () => multiplayerClient.Room?.State == MultiplayerRoomState.Playing); AddUntilStep("wait for local user to enter spectator", () => multiplayerComponents.CurrentScreen is MultiSpectatorScreen); } } @@ -935,7 +935,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("click ready button", () => { - user = playingUserId == null ? client.LocalUser : client.Room?.Users.Single(u => u.UserID == playingUserId); + user = playingUserId == null ? multiplayerClient.LocalUser : multiplayerClient.Room?.Users.Single(u => u.UserID == playingUserId); lastState = user?.State ?? MultiplayerUserState.Idle; InputManager.MoveMouseTo(readyButton); @@ -955,7 +955,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 437f4e3308..6605f82d5c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (int user in users) { SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); - multiplayerUsers.Add(OnlinePlayDependencies.Client.AddUser(new APIUser { Id = user }, true)); + multiplayerUsers.Add(OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true)); } Children = new Drawable[] @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for load", () => leaderboard.IsLoaded); - AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0); + AddUntilStep("wait for user population", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count > 0); } [Test] @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestUserQuit() { foreach (int user in users) - AddStep($"mark user {user} quit", () => Client.RemoveUser(UserLookupCache.GetUserAsync(user).GetResultSafely().AsNonNull())); + AddStep($"mark user {user} quit", () => MultiplayerClient.RemoveUser(UserLookupCache.GetUserAsync(user).GetResultSafely().AsNonNull())); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index cd3ae50dab..dabc1c1e5a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (int user in users) { SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); - var roomUser = OnlinePlayDependencies.Client.AddUser(new APIUser { Id = user }, true); + var roomUser = OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true); roomUser.MatchState = new TeamVersusUserState { @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for load", () => leaderboard.IsLoaded); - AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0); + AddUntilStep("wait for user population", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count > 0); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 7c18ed2572..7d2ef8276d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); - AddStep("select swap mod", () => Client.ChangeUserMods(API.LocalUser.Value.OnlineID, new[] { new TaikoModSwap() })); + AddStep("select swap mod", () => MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, new[] { new TaikoModSwap() })); AddUntilStep("participant panel has mod", () => this.ChildrenOfType().Any(p => p.ChildrenOfType().Any(m => m.Mod is TaikoModSwap))); } @@ -141,17 +141,17 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user (ready)", () => { - Client.AddUser(new APIUser { Id = PLAYER_1_ID }); - Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready); + MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }); + MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready); }); ClickButtonWhenEnabled(); - AddUntilStep("wait for spectating user state", () => Client.LocalUser?.State == MultiplayerUserState.Spectating); + AddUntilStep("wait for spectating user state", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); ClickButtonWhenEnabled(); - AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); + AddUntilStep("match started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 671b85164b..292319171d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1); - AddStep("add user", () => Client.AddUser(new APIUser + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, Username = "Second", @@ -50,15 +50,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1); - AddStep("add non-resolvable user", () => Client.TestAddUnresolvedUser()); - AddAssert("null user added", () => Client.Room.AsNonNull().Users.Count(u => u.User == null) == 1); + AddStep("add non-resolvable user", () => MultiplayerClient.TestAddUnresolvedUser()); + AddAssert("null user added", () => MultiplayerClient.Room.AsNonNull().Users.Count(u => u.User == null) == 1); AddUntilStep("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2); AddStep("kick null user", () => this.ChildrenOfType().Single(p => p.User.User == null) .ChildrenOfType().Single().TriggerClick()); - AddAssert("null user kicked", () => Client.Room.AsNonNull().Users.Count == 1); + AddAssert("null user kicked", () => MultiplayerClient.Room.AsNonNull().Users.Count == 1); } [Test] @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add a user", () => { - Client.AddUser(secondUser = new APIUser + MultiplayerClient.AddUser(secondUser = new APIUser { Id = 3, Username = "Second", @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); }); - AddStep("remove host", () => Client.RemoveUser(API.LocalUser.Value)); + AddStep("remove host", () => MultiplayerClient.RemoveUser(API.LocalUser.Value)); AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.User == secondUser); } @@ -84,21 +84,21 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestGameStateHasPriorityOverDownloadState() { - AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); checkProgressBarVisibility(true); - AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Results)); + AddStep("make user ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Results)); checkProgressBarVisibility(false); AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent); - AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Idle)); + AddStep("make user ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Idle)); checkProgressBarVisibility(true); } [Test] public void TestCorrectInitialState() { - AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); createNewParticipantsList(); checkProgressBarVisibility(true); } @@ -106,23 +106,23 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestBeatmapDownloadingStates() { - AddStep("set to no map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); - AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); + AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); checkProgressBarVisibility(true); AddRepeatStep("increment progress", () => { float progress = this.ChildrenOfType().Single().User.BeatmapAvailability.DownloadProgress ?? 0; - Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f))); + MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f))); }, 25); AddAssert("progress bar increased", () => this.ChildrenOfType().Single().Current.Value > 0); - AddStep("set to importing map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Importing())); + AddStep("set to importing map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Importing())); checkProgressBarVisibility(false); - AddStep("set to available", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable())); + AddStep("set to available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable())); } [Test] @@ -130,24 +130,24 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddAssert("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); - AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready)); + AddStep("make user ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Ready)); AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent); - AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle)); + AddStep("make user idle", () => MultiplayerClient.ChangeState(MultiplayerUserState.Idle)); AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); } [Test] public void TestToggleSpectateState() { - AddStep("make user spectating", () => Client.ChangeState(MultiplayerUserState.Spectating)); - AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle)); + AddStep("make user spectating", () => MultiplayerClient.ChangeState(MultiplayerUserState.Spectating)); + AddStep("make user idle", () => MultiplayerClient.ChangeState(MultiplayerUserState.Idle)); } [Test] public void TestCrownChangesStateWhenHostTransferred() { - AddStep("add user", () => Client.AddUser(new APIUser + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, Username = "Second", @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("first user crown visible", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 1); AddUntilStep("second user crown hidden", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 0); - AddStep("make second user host", () => Client.TransferHost(3)); + AddStep("make second user host", () => MultiplayerClient.TransferHost(3)); AddUntilStep("first user crown hidden", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 0); AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1); @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestKickButtonOnlyPresentWhenHost() { - AddStep("add user", () => Client.AddUser(new APIUser + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, Username = "Second", @@ -175,11 +175,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1); - AddStep("make second user host", () => Client.TransferHost(3)); + AddStep("make second user host", () => MultiplayerClient.TransferHost(3)); AddUntilStep("kick buttons not visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 0); - AddStep("make local user host again", () => Client.TransferHost(API.LocalUser.Value.Id)); + AddStep("make local user host again", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id)); AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1); } @@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestKickButtonKicks() { - AddStep("add user", () => Client.AddUser(new APIUser + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, Username = "Second", @@ -196,7 +196,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("kick second user", () => this.ChildrenOfType().Single(d => d.IsPresent).TriggerClick()); - AddAssert("second user kicked", () => Client.Room?.Users.Single().UserID == API.LocalUser.Value.Id); + AddAssert("second user kicked", () => MultiplayerClient.Room?.Users.Single().UserID == API.LocalUser.Value.Id); } [Test] @@ -206,7 +206,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { for (int i = 0; i < 20; i++) { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = i, Username = $"User {i}", @@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Multiplayer CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); - Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1)); + MultiplayerClient.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1)); if (RNG.NextBool()) { @@ -229,15 +229,15 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (beatmapState) { case DownloadState.NotDownloaded: - Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.NotDownloaded()); + MultiplayerClient.ChangeUserBeatmapAvailability(i, BeatmapAvailability.NotDownloaded()); break; case DownloadState.Downloading: - Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Downloading(RNG.NextSingle())); + MultiplayerClient.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Downloading(RNG.NextSingle())); break; case DownloadState.Importing: - Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Importing()); + MultiplayerClient.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Importing()); break; } } @@ -250,7 +250,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add user", () => { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 0, Username = "User 0", @@ -264,7 +264,7 @@ namespace osu.Game.Tests.Visual.Multiplayer CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); - Client.ChangeUserMods(0, new Mod[] + MultiplayerClient.ChangeUserMods(0, new Mod[] { new OsuModHardRock(), new OsuModDifficultyAdjust { ApproachRate = { Value = 1 } } @@ -274,12 +274,12 @@ namespace osu.Game.Tests.Visual.Multiplayer for (var i = MultiplayerUserState.Idle; i < MultiplayerUserState.Results; i++) { var state = i; - AddStep($"set state: {state}", () => Client.ChangeUserState(0, state)); + AddStep($"set state: {state}", () => MultiplayerClient.ChangeUserState(0, state)); } - AddStep("set state: downloading", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.Downloading(0))); + AddStep("set state: downloading", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.Downloading(0))); - AddStep("set state: locally available", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable())); + AddStep("set state: locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable())); } [Test] @@ -287,7 +287,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add dummy mods", () => { - Client.ChangeUserMods(new Mod[] + MultiplayerClient.ChangeUserMods(new Mod[] { new OsuModNoFail(), new OsuModDoubleTime() @@ -296,7 +296,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add user with mods", () => { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 0, Username = "Baka", @@ -309,34 +309,34 @@ namespace osu.Game.Tests.Visual.Multiplayer }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); - Client.ChangeUserMods(0, new Mod[] + MultiplayerClient.ChangeUserMods(0, new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }); }); - AddStep("set 0 ready", () => Client.ChangeState(MultiplayerUserState.Ready)); + AddStep("set 0 ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Ready)); - AddStep("set 1 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating)); + AddStep("set 1 spectate", () => MultiplayerClient.ChangeUserState(0, MultiplayerUserState.Spectating)); // Have to set back to idle due to status priority. AddStep("set 0 no map, 1 ready", () => { - Client.ChangeState(MultiplayerUserState.Idle); - Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()); - Client.ChangeUserState(0, MultiplayerUserState.Ready); + MultiplayerClient.ChangeState(MultiplayerUserState.Idle); + MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()); + MultiplayerClient.ChangeUserState(0, MultiplayerUserState.Ready); }); - AddStep("set 0 downloading", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + AddStep("set 0 downloading", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); - AddStep("set 0 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating)); + AddStep("set 0 spectate", () => MultiplayerClient.ChangeUserState(0, MultiplayerUserState.Spectating)); AddStep("make both default", () => { - Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()); - Client.ChangeUserState(0, MultiplayerUserState.Idle); - Client.ChangeState(MultiplayerUserState.Idle); + MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()); + MultiplayerClient.ChangeUserState(0, MultiplayerUserState.Idle); + MultiplayerClient.ChangeState(MultiplayerUserState.Idle); }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 010e9dc078..d391fbcf8f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -28,15 +28,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("initialise gameplay", () => { - Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem + Stack.Push(player = new MultiplayerPlayer(MultiplayerClient.APIRoom, new PlaylistItem { Beatmap = { Value = Beatmap.Value.BeatmapInfo }, RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, - }, Client.Room?.Users.ToArray())); + }, MultiplayerClient.Room?.Users.ToArray())); }); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded); - AddStep("start gameplay", () => ((IMultiplayerClient)Client).MatchStarted()); + AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).MatchStarted()); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 7bc5990c32..f84abc7443 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Multiplayer importedBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); }); - AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); + AddStep("change to all players mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); } [Test] @@ -97,19 +97,19 @@ namespace osu.Game.Tests.Visual.Multiplayer addItemStep(); addItemStep(); - AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); + AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(1, 0); assertItemInQueueListStep(2, 0); assertItemInQueueListStep(3, 1); - AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); + AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(2, 0); assertItemInHistoryListStep(1, 1); assertItemInQueueListStep(3, 0); - AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); + AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(3, 0); assertItemInHistoryListStep(2, 1); @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestListsClearedWhenRoomLeft() { addItemStep(); - AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); + AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); AddStep("leave room", () => RoomManager.PartRoom()); AddUntilStep("wait for room part", () => !RoomJoined); @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Multiplayer /// /// Adds a step to create a new playlist item. /// - private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem + private void addItemStep(bool expired = false) => AddStep("add item", () => MultiplayerClient.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem { Beatmap = { Value = importedBeatmap }, BeatmapID = importedBeatmap.OnlineID, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 0b3e1ba20a..bf06b6ad73 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(500, 300), - Items = { BindTarget = Client.APIRoom!.Playlist } + Items = { BindTarget = MultiplayerClient.APIRoom!.Playlist } }; }); @@ -61,14 +61,14 @@ namespace osu.Game.Tests.Visual.Multiplayer importedBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); }); - AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); + AddStep("change to all players mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); } [Test] public void TestDeleteButtonAlwaysVisibleForHost() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); - AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); + AddUntilStep("wait for queue mode change", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); addPlaylistItem(() => API.LocalUser.Value.OnlineID); assertDeleteButtonVisibility(1, true); @@ -79,18 +79,18 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); - AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); + AddUntilStep("wait for queue mode change", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); - AddStep("join other user", () => Client.AddUser(new APIUser { Id = 1234 })); - AddStep("set other user as host", () => Client.TransferHost(1234)); + AddStep("join other user", () => MultiplayerClient.AddUser(new APIUser { Id = 1234 })); + AddStep("set other user as host", () => MultiplayerClient.TransferHost(1234)); addPlaylistItem(() => API.LocalUser.Value.OnlineID); assertDeleteButtonVisibility(1, true); addPlaylistItem(() => 1234); assertDeleteButtonVisibility(2, false); - AddStep("set local user as host", () => Client.TransferHost(API.LocalUser.Value.OnlineID)); + AddStep("set local user as host", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); assertDeleteButtonVisibility(1, true); assertDeleteButtonVisibility(2, true); } @@ -98,16 +98,16 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestCurrentItemDoesNotHaveDeleteButton() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); - AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); + AddUntilStep("wait for queue mode change", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); addPlaylistItem(() => API.LocalUser.Value.OnlineID); assertDeleteButtonVisibility(0, false); assertDeleteButtonVisibility(1, true); - AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); - AddUntilStep("wait for next item to be selected", () => Client.Room?.Settings.PlaylistItemId == 2); + AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); + AddUntilStep("wait for next item to be selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == 2); AddUntilStep("wait for two items in playlist", () => playlist.ChildrenOfType().Count() == 2); assertDeleteButtonVisibility(0, false); @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapID = importedBeatmap.OnlineID, }); - Client.AddUserPlaylistItem(userId(), item).WaitSafely(); + MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely(); itemId = item.ID; }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 7834226f15..609693e54b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -74,13 +74,13 @@ namespace osu.Game.Tests.Visual.Multiplayer Task.Run(async () => { - if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) + if (MultiplayerClient.IsHost && MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready) { - await Client.StartMatch(); + await MultiplayerClient.StartMatch(); return; } - await Client.ToggleReady(); + await MultiplayerClient.ToggleReady(); readyClickOperation.Dispose(); }); @@ -110,15 +110,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add second user as host", () => { - Client.AddUser(new APIUser { Id = 2, Username = "Another user" }); - Client.TransferHost(2); + MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); + MultiplayerClient.TransferHost(2); }); ClickButtonWhenEnabled(); - AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); ClickButtonWhenEnabled(); - AddUntilStep("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } [TestCase(true)] @@ -127,14 +127,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("setup", () => { - Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0); if (!allReady) - Client.AddUser(new APIUser { Id = 2, Username = "Another user" }); + MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); ClickButtonWhenEnabled(); - AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); verifyGameplayStartFlow(); } @@ -144,12 +144,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add host", () => { - Client.AddUser(new APIUser { Id = 2, Username = "Another user" }); - Client.TransferHost(2); + MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); + MultiplayerClient.TransferHost(2); }); ClickButtonWhenEnabled(); - AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0)); + AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0)); verifyGameplayStartFlow(); } @@ -159,17 +159,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("setup", () => { - Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); - Client.AddUser(new APIUser { Id = 2, Username = "Another user" }); + MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0); + MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); ClickButtonWhenEnabled(); - AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0)); + AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0)); ClickButtonWhenEnabled(); - AddUntilStep("user is idle (match not started)", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); } @@ -180,42 +180,42 @@ namespace osu.Game.Tests.Visual.Multiplayer const int users = 10; AddStep("setup", () => { - Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0); for (int i = 0; i < users; i++) - Client.AddUser(new APIUser { Id = i, Username = "Another user" }); + MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" }); }); if (!isHost) - AddStep("transfer host", () => Client.TransferHost(2)); + AddStep("transfer host", () => MultiplayerClient.TransferHost(2)); ClickButtonWhenEnabled(); AddRepeatStep("change user ready state", () => { - Client.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle); + MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle); }, 20); AddRepeatStep("ready all users", () => { - var nextUnready = Client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); + var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); if (nextUnready != null) - Client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); + MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); }, users); } private void verifyGameplayStartFlow() { - AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); ClickButtonWhenEnabled(); - AddUntilStep("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddAssert("ready button disabled", () => !button.ChildrenOfType().Single().Enabled.Value); AddStep("transitioned to gameplay", () => readyClickOperation.Dispose()); AddStep("finish gameplay", () => { - Client.ChangeUserState(Client.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded); - Client.ChangeUserState(Client.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay); + MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded); + MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay); }); AddUntilStep("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 70d4d9dd55..c4ea78116a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Task.Run(async () => { - await Client.ToggleSpectate(); + await MultiplayerClient.ToggleSpectate(); readyClickOperation.Dispose(); }); } @@ -94,13 +94,13 @@ namespace osu.Game.Tests.Visual.Multiplayer Task.Run(async () => { - if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) + if (MultiplayerClient.IsHost && MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready) { - await Client.StartMatch(); + await MultiplayerClient.StartMatch(); return; } - await Client.ToggleReady(); + await MultiplayerClient.ToggleReady(); readyClickOperation.Dispose(); }); @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [TestCase(MultiplayerRoomState.Playing)] public void TestEnabledWhenRoomOpenOrInGameplay(MultiplayerRoomState roomState) { - AddStep($"change room to {roomState}", () => Client.ChangeRoomState(roomState)); + AddStep($"change room to {roomState}", () => MultiplayerClient.ChangeRoomState(roomState)); assertSpectateButtonEnablement(true); } @@ -124,16 +124,16 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestToggleWhenIdle(MultiplayerUserState initialState) { ClickButtonWhenEnabled(); - AddUntilStep("user is spectating", () => Client.Room?.Users[0].State == MultiplayerUserState.Spectating); + AddUntilStep("user is spectating", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Spectating); ClickButtonWhenEnabled(); - AddUntilStep("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } [TestCase(MultiplayerRoomState.Closed)] public void TestDisabledWhenClosed(MultiplayerRoomState roomState) { - AddStep($"change room to {roomState}", () => Client.ChangeRoomState(roomState)); + AddStep($"change room to {roomState}", () => MultiplayerClient.ChangeRoomState(roomState)); assertSpectateButtonEnablement(false); } @@ -147,8 +147,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestReadyButtonEnabledWhenHostAndUsersReady() { - AddStep("add user", () => Client.AddUser(new APIUser { Id = PLAYER_1_ID })); - AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID })); + AddStep("set user ready", () => MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); ClickButtonWhenEnabled(); assertReadyButtonEnablement(true); @@ -159,11 +159,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add user and transfer host", () => { - Client.AddUser(new APIUser { Id = PLAYER_1_ID }); - Client.TransferHost(PLAYER_1_ID); + MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }); + MultiplayerClient.TransferHost(PLAYER_1_ID); }); - AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); + AddStep("set user ready", () => MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); ClickButtonWhenEnabled(); assertReadyButtonEnablement(false); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs index 823ac07cf7..f95e73ff3c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs @@ -25,14 +25,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add user", () => { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 2, Statistics = { GlobalRank = 1234 } }); // Remove the local user so only the one above is displayed. - Client.RemoveUser(API.LocalUser.Value); + MultiplayerClient.RemoveUser(API.LocalUser.Value); }); } @@ -41,26 +41,26 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add users", () => { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 2, Statistics = { GlobalRank = 1234 } }); - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 3, Statistics = { GlobalRank = 3333 } }); - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 4, Statistics = { GlobalRank = 4321 } }); // Remove the local user so only the ones above are displayed. - Client.RemoveUser(API.LocalUser.Value); + MultiplayerClient.RemoveUser(API.LocalUser.Value); }); } @@ -75,20 +75,20 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add users", () => { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 2, Statistics = { GlobalRank = min } }); - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 3, Statistics = { GlobalRank = max } }); // Remove the local user so only the ones above are displayed. - Client.RemoveUser(API.LocalUser.Value); + MultiplayerClient.RemoveUser(API.LocalUser.Value); }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 7a5bd0b8f4..50b3f52047 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerComponents multiplayerComponents; - private TestMultiplayerClient client => multiplayerComponents.Client; + private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -75,8 +75,8 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); - AddAssert("user state arrived", () => client.Room?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState); + AddUntilStep("room type is team vs", () => multiplayerClient.Room?.Settings.MatchType == MatchType.TeamVersus); + AddAssert("user state arrived", () => multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState); } [Test] @@ -96,25 +96,25 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); - AddStep("add another user", () => client.AddUser(new APIUser { Username = "otheruser", Id = 44 })); + AddAssert("user on team 0", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); + AddStep("add another user", () => multiplayerClient.AddUser(new APIUser { Username = "otheruser", Id = 44 })); AddStep("press own button", () => { InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); - AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1); + AddAssert("user on team 1", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1); AddStep("press own button again", () => InputManager.Click(MouseButton.Left)); - AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); + AddAssert("user on team 0", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); AddStep("press other user's button", () => { InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType().ElementAt(1)); InputManager.Click(MouseButton.Left); }); - AddAssert("user still on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); + AddAssert("user still on team 0", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); } [Test] @@ -134,14 +134,14 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddUntilStep("match type head to head", () => client.APIRoom?.Type.Value == MatchType.HeadToHead); + AddUntilStep("match type head to head", () => multiplayerClient.APIRoom?.Type.Value == MatchType.HeadToHead); - AddStep("change match type", () => client.ChangeSettings(new MultiplayerRoomSettings + AddStep("change match type", () => multiplayerClient.ChangeSettings(new MultiplayerRoomSettings { MatchType = MatchType.TeamVersus }).WaitSafely()); - AddUntilStep("api room updated to team versus", () => client.APIRoom?.Type.Value == MatchType.TeamVersus); + AddUntilStep("api room updated to team versus", () => multiplayerClient.APIRoom?.Type.Value == MatchType.TeamVersus); } [Test] @@ -160,13 +160,13 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddUntilStep("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead); + AddUntilStep("room type is head to head", () => multiplayerClient.Room?.Settings.MatchType == MatchType.HeadToHead); AddUntilStep("team displays are not displaying teams", () => multiplayerComponents.ChildrenOfType().All(d => d.DisplayedTeam == null)); - AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus)); + AddStep("change to team vs", () => multiplayerClient.ChangeSettings(matchType: MatchType.TeamVersus)); - AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); + AddUntilStep("room type is team vs", () => multiplayerClient.Room?.Settings.MatchType == MatchType.TeamVersus); AddUntilStep("team displays are displaying teams", () => multiplayerComponents.ChildrenOfType().All(d => d.DisplayedTeam != null)); } @@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); } } } diff --git a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs index bd8fb8e58e..062aa33b99 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual public new bool IsLoaded => base.IsLoaded && MultiplayerScreen.IsLoaded; [Cached(typeof(MultiplayerClient))] - public readonly TestMultiplayerClient Client; + public readonly TestMultiplayerClient MultiplayerClient; [Cached(typeof(UserLookupCache))] private readonly UserLookupCache userLookupCache = new TestUserLookupCache(); @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual { userLookupCache, beatmapLookupCache, - Client = new TestMultiplayerClient(RoomManager), + MultiplayerClient = new TestMultiplayerClient(RoomManager), screenStack = new OsuScreenStack { Name = nameof(TestMultiplayerComponents), diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs index f166154103..62d1c9ceca 100644 --- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; @@ -14,9 +13,9 @@ namespace osu.Game.Tests.Visual.Multiplayer public interface IMultiplayerTestSceneDependencies : IOnlinePlayTestSceneDependencies { /// - /// The cached . + /// The cached . /// - TestMultiplayerClient Client { get; } + TestMultiplayerClient MultiplayerClient { get; } /// /// The cached . diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index a9b3ca4991..19f9952d14 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -17,13 +17,13 @@ namespace osu.Game.Tests.Visual.Multiplayer public const int PLAYER_1_ID = 55; public const int PLAYER_2_ID = 56; - public TestMultiplayerClient Client => OnlinePlayDependencies.Client; + public TestMultiplayerClient MultiplayerClient => OnlinePlayDependencies.MultiplayerClient; public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager; public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient; protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; - public bool RoomJoined => Client.RoomJoined; + public bool RoomJoined => MultiplayerClient.RoomJoined; private readonly bool joinRoom; diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs index d9fe77ae44..6b4e01b673 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs @@ -14,16 +14,16 @@ namespace osu.Game.Tests.Visual.Multiplayer /// public class MultiplayerTestSceneDependencies : OnlinePlayTestSceneDependencies, IMultiplayerTestSceneDependencies { - public TestMultiplayerClient Client { get; } + public TestMultiplayerClient MultiplayerClient { get; } public TestSpectatorClient SpectatorClient { get; } public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager; public MultiplayerTestSceneDependencies() { - Client = new TestMultiplayerClient(RoomManager); + MultiplayerClient = new TestMultiplayerClient(RoomManager); SpectatorClient = CreateSpectatorClient(); - CacheAs(Client); + CacheAs(MultiplayerClient); CacheAs(SpectatorClient); } From 5db63a8751e38d9701b9987ae30508b9be516475 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 11:30:03 +0900 Subject: [PATCH 565/587] Expose read-only list from request --- osu.Game/Online/API/Requests/GetBeatmapsRequest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs index c07e5ef1c2..22af022659 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs @@ -2,12 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; namespace osu.Game.Online.API.Requests { public class GetBeatmapsRequest : APIRequest { - public readonly int[] BeatmapIds; + public readonly IReadOnlyList BeatmapIds; private const int max_ids_per_request = 50; From 84e82ef5e466d1802f1273e72a389cdf1f54c6fd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 14:09:19 +0900 Subject: [PATCH 566/587] Add XMLDocs to difficulty attribute properties --- .../Difficulty/CatchDifficultyAttributes.cs | 6 +++ .../Difficulty/ManiaDifficultyAttributes.cs | 9 +++++ .../Difficulty/OsuDifficultyAttributes.cs | 38 +++++++++++++++++++ .../Difficulty/TaikoDifficultyAttributes.cs | 21 ++++++++++ .../Difficulty/DifficultyAttributes.cs | 2 +- 5 files changed, 75 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index 39a58d336d..8e069d7d16 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -9,6 +9,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchDifficultyAttributes : DifficultyAttributes { + /// + /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("approach_rate")] public double ApproachRate { get; set; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index 979a04ddf8..efd5868504 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -9,9 +9,18 @@ namespace osu.Game.Rulesets.Mania.Difficulty { public class ManiaDifficultyAttributes : DifficultyAttributes { + /// + /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("great_hit_window")] public double GreatHitWindow { get; set; } + /// + /// The score multiplier applied via score-reducing mods. + /// [JsonProperty("score_multiplier")] public double ScoreMultiplier { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 126a9b0183..3deed4ea3d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -12,30 +12,68 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuDifficultyAttributes : DifficultyAttributes { + /// + /// The difficulty corresponding to the aim skill. + /// [JsonProperty("aim_difficulty")] public double AimDifficulty { get; set; } + /// + /// The difficulty corresponding to the speed skill. + /// [JsonProperty("speed_difficulty")] public double SpeedDifficulty { get; set; } + /// + /// The difficulty corresponding to the flashlight skill. + /// [JsonProperty("flashlight_difficulty")] public double FlashlightDifficulty { get; set; } + /// + /// Describes how much of is contributed to by hitcircles or sliders. + /// A value closer to 1.0 indicates most of is contributed by hitcircles. + /// A value closer to 0.0 indicates most of is contributed by sliders. + /// [JsonProperty("slider_factor")] public double SliderFactor { get; set; } + /// + /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("approach_rate")] public double ApproachRate { get; set; } + /// + /// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("overall_difficulty")] public double OverallDifficulty { get; set; } + /// + /// The beatmap's drain rate. This doesn't scale with rate-adjusting mods. + /// public double DrainRate { get; set; } + /// + /// The number of hitcircles in the beatmap. + /// public int HitCircleCount { get; set; } + /// + /// The number of sliders in the beatmap. + /// public int SliderCount { get; set; } + /// + /// The number of spinners in the beatmap. + /// public int SpinnerCount { get; set; } public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 31f5a6f570..3dc5438072 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -9,18 +9,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyAttributes : DifficultyAttributes { + /// + /// The difficulty corresponding to the stamina skill. + /// [JsonProperty("stamina_difficulty")] public double StaminaDifficulty { get; set; } + /// + /// The difficulty corresponding to the rhythm skill. + /// [JsonProperty("rhythm_difficulty")] public double RhythmDifficulty { get; set; } + /// + /// The difficulty corresponding to the colour skill. + /// [JsonProperty("colour_difficulty")] public double ColourDifficulty { get; set; } + /// + /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("approach_rate")] public double ApproachRate { get; set; } + /// + /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("great_hit_window")] public double GreatHitWindow { get; set; } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 991b567f57..ec3d22b67a 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Difficulty public Mod[] Mods { get; set; } /// - /// The combined star rating of all skill. + /// The combined star rating of all skills. /// [JsonProperty("star_rating", Order = -3)] public double StarRating { get; set; } From f703828e1bc17e3ec5421dbc09ea9a62ca3f8437 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 15:27:48 +0900 Subject: [PATCH 567/587] Clarify ambiguous conditionals in `LegacyStageBackground` --- .../Skinning/Legacy/LegacyStageBackground.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs index 952fc7ddd6..fdacc75c92 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs @@ -98,8 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy float rightLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1; bool hasLeftLine = leftLineWidth > 0; - bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value >= 2.4m - || isLastColumn; + bool hasRightLine = (rightLineWidth > 0 && skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value >= 2.4m) || isLastColumn; Color4 lineColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White; Color4 backgroundColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, columnIndex)?.Value ?? Color4.Black; From edd31bf3aa913a7b01306538f88a6c44d160d729 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 15:51:18 +0900 Subject: [PATCH 568/587] Revert styling change --- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index faeb73cfa3..8d840280b3 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -430,34 +430,31 @@ namespace osu.Game.Screens.OnlinePlay }; } - private IEnumerable createButtons() + private IEnumerable createButtons() => new[] { - return new[] + showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) { - showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) - { - Size = new Vector2(30, 30), - Action = () => RequestResults?.Invoke(Item), - Alpha = AllowShowingResults ? 1 : 0, - TooltipText = "View results" - }, - beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap), - editButton = new PlaylistEditButton - { - Size = new Vector2(30, 30), - Alpha = AllowEditing ? 1 : 0, - Action = () => RequestEdit?.Invoke(Item), - TooltipText = "Edit" - }, - removeButton = new PlaylistRemoveButton - { - Size = new Vector2(30, 30), - Alpha = AllowDeletion ? 1 : 0, - Action = () => RequestDeletion?.Invoke(Item), - TooltipText = "Remove from playlist" - }, - }; - } + Size = new Vector2(30, 30), + Action = () => RequestResults?.Invoke(Item), + Alpha = AllowShowingResults ? 1 : 0, + TooltipText = "View results" + }, + beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap), + editButton = new PlaylistEditButton + { + Size = new Vector2(30, 30), + Alpha = AllowEditing ? 1 : 0, + Action = () => RequestEdit?.Invoke(Item), + TooltipText = "Edit" + }, + removeButton = new PlaylistRemoveButton + { + Size = new Vector2(30, 30), + Alpha = AllowDeletion ? 1 : 0, + Action = () => RequestDeletion?.Invoke(Item), + TooltipText = "Remove from playlist" + }, + }; protected override bool OnClick(ClickEvent e) { From 55d9f0b44b507d4ff8140271e7fb3807ad2c4ef2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 16:03:08 +0900 Subject: [PATCH 569/587] Store beatmap to a field instead --- .../OnlinePlayBeatmapAvailabilityTracker.cs | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 02bcd1f30c..07506ba1f0 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -46,10 +46,9 @@ namespace osu.Game.Online.Rooms private readonly Bindable availability = new Bindable(BeatmapAvailability.NotDownloaded()); private ScheduledDelegate progressUpdate; - private BeatmapDownloadTracker downloadTracker; - private IDisposable realmSubscription; + private APIBeatmap selectedBeatmap; protected override void LoadComplete() { @@ -63,26 +62,30 @@ namespace osu.Game.Online.Rooms return; downloadTracker?.RemoveAndDisposeImmediately(); + selectedBeatmap = null; beatmapLookupCache.GetBeatmapAsync(item.NewValue.Beatmap.OnlineID).ContinueWith(task => Schedule(() => { var beatmap = task.GetResultSafely(); if (SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID) - beginTracking(beatmap); + { + selectedBeatmap = beatmap; + beginTracking(); + } }), TaskContinuationOptions.OnlyOnRanToCompletion); }, true); } - private void beginTracking(APIBeatmap beatmap) + private void beginTracking() { - Debug.Assert(beatmap.BeatmapSet != null); + Debug.Assert(selectedBeatmap.BeatmapSet != null); - downloadTracker = new BeatmapDownloadTracker(beatmap.BeatmapSet); + downloadTracker = new BeatmapDownloadTracker(selectedBeatmap.BeatmapSet); AddInternal(downloadTracker); - downloadTracker.State.BindValueChanged(_ => Scheduler.AddOnce(updateAvailability, beatmap), true); + downloadTracker.State.BindValueChanged(_ => Scheduler.AddOnce(updateAvailability), true); downloadTracker.Progress.BindValueChanged(_ => { if (downloadTracker.State.Value != DownloadState.Downloading) @@ -91,23 +94,23 @@ namespace osu.Game.Online.Rooms // incoming progress changes are going to be at a very high rate. // we don't want to flood the network with this, so rate limit how often we send progress updates. if (progressUpdate?.Completed != false) - progressUpdate = Scheduler.AddDelayed(updateAvailability, beatmap, progressUpdate == null ? 0 : 500); + progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); }, true); // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(r => filteredBeatmaps(beatmap), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => filteredBeatmaps(), (items, changes, ___) => { if (changes == null) return; - Scheduler.AddOnce(updateAvailability, beatmap); + Scheduler.AddOnce(updateAvailability); }); } - private void updateAvailability(APIBeatmap beatmap) + private void updateAvailability() { - if (downloadTracker == null) + if (downloadTracker == null || selectedBeatmap == null) return; switch (downloadTracker.State.Value) @@ -125,7 +128,7 @@ namespace osu.Game.Online.Rooms break; case DownloadState.LocallyAvailable: - bool available = filteredBeatmaps(beatmap).Any(); + bool available = filteredBeatmaps().Any(); availability.Value = available ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); @@ -140,10 +143,10 @@ namespace osu.Game.Online.Rooms } } - private IQueryable filteredBeatmaps(APIBeatmap beatmap) + private IQueryable filteredBeatmaps() { - int onlineId = beatmap.OnlineID; - string checksum = beatmap.MD5Hash; + int onlineId = selectedBeatmap.OnlineID; + string checksum = selectedBeatmap.MD5Hash; return realm.Realm .All() From d4bf335fccd31e921447b999a91d722998d51417 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 16:24:39 +0900 Subject: [PATCH 570/587] Use score multiplier attribute in ManiaPerformanceCalculator --- .../Difficulty/ManiaPerformanceCalculator.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 8a8c41bb8a..beba29b8bb 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -43,14 +43,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease); - - double scoreMultiplier = 1.0; - foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) - scoreMultiplier *= m.ScoreMultiplier; - - // Scale score up, so it's comparable to other keymods - scaledScore *= 1.0 / scoreMultiplier; + if (Attributes.ScoreMultiplier > 0) + { + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / Attributes.ScoreMultiplier; + } // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // The specific number has no intrinsic meaning and can be adjusted as needed. @@ -80,6 +77,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeDifficultyValue() { + if (Attributes.ScoreMultiplier <= 0) + return 0; + double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); From 5dd9771c5fbb2ae68efe146b769375a6210b00d7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 16:27:27 +0900 Subject: [PATCH 571/587] Remove mod multipliers from being applied to scores --- .../TestSceneFooterButtonMods.cs | 27 ++----------------- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 13 +-------- osu.Game/Screens/Select/FooterButtonMods.cs | 25 ----------------- 3 files changed, 3 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 0631059d1a..e9014c0941 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Linq; using NUnit.Framework; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Select; @@ -14,11 +12,11 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneFooterButtonMods : OsuTestScene { - private readonly TestFooterButtonMods footerButtonMods; + private readonly FooterButtonMods footerButtonMods; public TestSceneFooterButtonMods() { - Add(footerButtonMods = new TestFooterButtonMods()); + Add(footerButtonMods = new FooterButtonMods()); } [Test] @@ -26,19 +24,15 @@ namespace osu.Game.Tests.Visual.UserInterface { var hiddenMod = new Mod[] { new OsuModHidden() }; AddStep(@"Add Hidden", () => changeMods(hiddenMod)); - AddAssert(@"Check Hidden multiplier", () => assertModsMultiplier(hiddenMod)); var hardRockMod = new Mod[] { new OsuModHardRock() }; AddStep(@"Add HardRock", () => changeMods(hardRockMod)); - AddAssert(@"Check HardRock multiplier", () => assertModsMultiplier(hardRockMod)); var doubleTimeMod = new Mod[] { new OsuModDoubleTime() }; AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod)); - AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod)); var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods)); - AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods)); } [Test] @@ -46,15 +40,12 @@ namespace osu.Game.Tests.Visual.UserInterface { var easyMod = new Mod[] { new OsuModEasy() }; AddStep(@"Add Easy", () => changeMods(easyMod)); - AddAssert(@"Check Easy multiplier", () => assertModsMultiplier(easyMod)); var noFailMod = new Mod[] { new OsuModNoFail() }; AddStep(@"Add NoFail", () => changeMods(noFailMod)); - AddAssert(@"Check NoFail multiplier", () => assertModsMultiplier(noFailMod)); var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() }; AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods)); - AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleDecrementMods)); } [Test] @@ -63,25 +54,11 @@ namespace osu.Game.Tests.Visual.UserInterface var multipleMods = new Mod[] { new OsuModDoubleTime(), new OsuModFlashlight() }; AddStep(@"Add mods", () => changeMods(multipleMods)); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); - AddAssert(@"Check empty multiplier", () => assertModsMultiplier(Array.Empty())); } private void changeMods(IReadOnlyList mods) { footerButtonMods.Current.Value = mods; } - - private bool assertModsMultiplier(IEnumerable mods) - { - double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); - string expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; - - return expectedValue == footerButtonMods.MultiplierText.Current.Value; - } - - private class TestFooterButtonMods : FooterButtonMods - { - public new OsuSpriteText MultiplierText => base.MultiplierText; - } } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 79861c0ecc..ce1486b02f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -92,8 +92,6 @@ namespace osu.Game.Rulesets.Scoring private readonly List hitEvents = new List(); private HitObject lastHitObject; - private double scoreMultiplier = 1; - public ScoreProcessor() { accuracyPortion = DefaultAccuracyPortion; @@ -111,15 +109,6 @@ namespace osu.Game.Rulesets.Scoring }; Mode.ValueChanged += _ => updateScore(); - Mods.ValueChanged += mods => - { - scoreMultiplier = 1; - - foreach (var m in mods.NewValue) - scoreMultiplier *= m.ScoreMultiplier; - - updateScore(); - }; } private readonly Dictionary scoreResultCounts = new Dictionary(); @@ -235,7 +224,7 @@ namespace osu.Game.Rulesets.Scoring case ScoringMode.Standardised: double accuracyScore = accuracyPortion * accuracyRatio; double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier; + return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)); case ScoringMode.Classic: // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 5bbca5ca1a..1f1aa4c4b3 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -6,14 +6,11 @@ using osu.Framework.Graphics; using osu.Game.Screens.Play.HUD; using osu.Game.Rulesets.Mods; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osuTK; -using osuTK.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Screens.Select @@ -26,10 +23,7 @@ namespace osu.Game.Screens.Select set => modDisplay.Current = value; } - protected readonly OsuSpriteText MultiplierText; private readonly ModDisplay modDisplay; - private Color4 lowMultiplierColour; - private Color4 highMultiplierColour; public FooterButtonMods() { @@ -40,12 +34,6 @@ namespace osu.Game.Screens.Select Scale = new Vector2(0.8f), ExpansionMode = ExpansionMode.AlwaysContracted, }); - ButtonContentContainer.Add(MultiplierText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - }); } [BackgroundDependencyLoader] @@ -53,8 +41,6 @@ namespace osu.Game.Screens.Select { SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); - lowMultiplierColour = colours.Red; - highMultiplierColour = colours.Green; Text = @"mods"; Hotkey = GlobalAction.ToggleModSelection; } @@ -68,17 +54,6 @@ namespace osu.Game.Screens.Select private void updateMultiplierText() { - double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; - - MultiplierText.Text = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; - - if (multiplier > 1.0) - MultiplierText.FadeColour(highMultiplierColour, 200); - else if (multiplier < 1.0) - MultiplierText.FadeColour(lowMultiplierColour, 200); - else - MultiplierText.FadeColour(Color4.White, 200); - if (Current.Value?.Count > 0) modDisplay.FadeIn(); else From 4c1413e0c793995d5a9ea4802419d9131d9abe53 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 16:36:02 +0900 Subject: [PATCH 572/587] No longer require Mod implementation --- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 7136795461..5555770822 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Mods /// The score multiplier of this mod. /// [JsonIgnore] - public abstract double ScoreMultiplier { get; } + public virtual double ScoreMultiplier => 1; /// /// Returns true if this mod is implemented (and playable). From 23933fc881f07c2cc96b10589531387e6b2658bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 17:32:22 +0900 Subject: [PATCH 573/587] Update xmldoc to mention that multipliers are not applied anywhere --- osu.Game/Rulesets/Mods/Mod.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 5555770822..9a45e2458d 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -80,8 +80,11 @@ namespace osu.Game.Rulesets.Mods } /// - /// The score multiplier of this mod. + /// The (legacy) score multiplier of this mod. /// + /// + /// This is not applied for newly set scores, but may be required for display purposes when showing legacy scores. + /// [JsonIgnore] public virtual double ScoreMultiplier => 1; From 28fcad92810097c4e4fb0cce0757bea2ec43d7b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 17:33:55 +0900 Subject: [PATCH 574/587] Update failing test to not account for no-longer-existing multiplier --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index de795241bf..0b9db8e20a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -147,8 +147,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("player score matching expected bonus score", () => { - // multipled by 2 to nullify the score multiplier. (autoplay mod selected) - double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; + double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value; return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); From a0ee86ddd2761e0e901235939bed9e3f6d4f1334 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 19:50:27 +0900 Subject: [PATCH 575/587] Fix improperly considering rate adjustment mods --- .../Difficulty/ManiaDifficultyAttributes.cs | 4 ++-- .../Difficulty/ManiaDifficultyCalculator.cs | 13 ++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index efd5868504..5b7a460079 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -10,10 +10,10 @@ namespace osu.Game.Rulesets.Mania.Difficulty public class ManiaDifficultyAttributes : DifficultyAttributes { /// - /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). + /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). /// /// - /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. + /// Rate-adjusting mods do not affect the hit window at all in osu-stable. /// [JsonProperty("great_hit_window")] public double GreatHitWindow { get; set; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 1f82eb7ccd..d200daa7fa 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty { StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, - GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate), + GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), ScoreMultiplier = getScoreMultiplier(mods), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), }; @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty } } - private int getHitWindow300(Mod[] mods) + private double getHitWindow300(Mod[] mods) { if (isForCurrentRuleset) { @@ -121,19 +121,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty return applyModAdjustments(47, mods); - static int applyModAdjustments(double value, Mod[] mods) + static double applyModAdjustments(double value, Mod[] mods) { if (mods.Any(m => m is ManiaModHardRock)) value /= 1.4; else if (mods.Any(m => m is ManiaModEasy)) value *= 1.4; - if (mods.Any(m => m is ManiaModDoubleTime)) - value *= 1.5; - else if (mods.Any(m => m is ManiaModHalfTime)) - value *= 0.75; - - return (int)value; + return value; } } From 0b37efc9850f57a6ddf456b0e68fc879a536a2e0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 20:07:26 +0900 Subject: [PATCH 576/587] Add explanatory note --- osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index d200daa7fa..b17aa7fc4d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -48,6 +48,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty { StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, + // In osu-stable mania, rate-adjustment mods don't affect the hit window. + // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), ScoreMultiplier = getScoreMultiplier(mods), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), From cb9ffc655acf4509ce174c770788b9bfeb715339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Feb 2022 22:32:42 +0100 Subject: [PATCH 577/587] Add tests showing expected behaviour of naming helper --- osu.Game.Tests/Utils/NamingUtilsTest.cs | 132 ++++++++++++++++++++++++ osu.Game/Utils/NamingUtils.cs | 15 +++ 2 files changed, 147 insertions(+) create mode 100644 osu.Game.Tests/Utils/NamingUtilsTest.cs create mode 100644 osu.Game/Utils/NamingUtils.cs diff --git a/osu.Game.Tests/Utils/NamingUtilsTest.cs b/osu.Game.Tests/Utils/NamingUtilsTest.cs new file mode 100644 index 0000000000..62e688db90 --- /dev/null +++ b/osu.Game.Tests/Utils/NamingUtilsTest.cs @@ -0,0 +1,132 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Utils; + +namespace osu.Game.Tests.Utils +{ + [TestFixture] + public class NamingUtilsTest + { + [Test] + public void TestEmptySet() + { + string nextBestName = NamingUtils.GetNextBestName(Enumerable.Empty(), "New Difficulty"); + + Assert.AreEqual("New Difficulty", nextBestName); + } + + [Test] + public void TestNotTaken() + { + string[] existingNames = + { + "Something", + "Entirely", + "Different" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty", nextBestName); + } + + [Test] + public void TestNotTakenButClose() + { + string[] existingNames = + { + "New Difficulty(1)", + "New Difficulty (abcd)", + "New Difficulty but not really" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty", nextBestName); + } + + [Test] + public void TestAlreadyTaken() + { + string[] existingNames = + { + "New Difficulty" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty (1)", nextBestName); + } + + [Test] + public void TestAlreadyTakenWithDifferentCase() + { + string[] existingNames = + { + "new difficulty" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty (1)", nextBestName); + } + + [Test] + public void TestAlreadyTakenWithBrackets() + { + string[] existingNames = + { + "new difficulty (copy)" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty (copy)"); + + Assert.AreEqual("New Difficulty (copy) (1)", nextBestName); + } + + [Test] + public void TestMultipleAlreadyTaken() + { + string[] existingNames = + { + "New Difficulty", + "New difficulty (1)", + "new Difficulty (2)", + "New DIFFICULTY (3)" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty (4)", nextBestName); + } + + [Test] + public void TestEvenMoreAlreadyTaken() + { + string[] existingNames = Enumerable.Range(1, 30).Select(i => $"New Difficulty ({i})").Append("New Difficulty").ToArray(); + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty (31)", nextBestName); + } + + [Test] + public void TestMultipleAlreadyTakenWithGaps() + { + string[] existingNames = + { + "New Difficulty", + "New Difficulty (1)", + "New Difficulty (4)", + "New Difficulty (9)" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty (2)", nextBestName); + } + } +} diff --git a/osu.Game/Utils/NamingUtils.cs b/osu.Game/Utils/NamingUtils.cs new file mode 100644 index 0000000000..1b48f57932 --- /dev/null +++ b/osu.Game/Utils/NamingUtils.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; + +namespace osu.Game.Utils +{ + public static class NamingUtils + { + public static string GetNextBestName(IEnumerable existingNames, string desiredName) + { + return null; + } + } +} From e09570c31bf18af340d529fc59448b1b694db270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Feb 2022 23:00:45 +0100 Subject: [PATCH 578/587] Implement best-name-finding helper method --- osu.Game/Utils/NamingUtils.cs | 48 ++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/osu.Game/Utils/NamingUtils.cs b/osu.Game/Utils/NamingUtils.cs index 1b48f57932..482e3d0954 100644 --- a/osu.Game/Utils/NamingUtils.cs +++ b/osu.Game/Utils/NamingUtils.cs @@ -2,14 +2,60 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Text.RegularExpressions; namespace osu.Game.Utils { public static class NamingUtils { + /// + /// Given a set of and a target , + /// finds a "best" name closest to that is not in . + /// + /// + /// + /// This helper is most useful in scenarios when creating new objects in a set + /// (such as adding new difficulties to a beatmap set, or creating a clone of an existing object that needs a unique name). + /// If is already present in , + /// this method will append the lowest possible number in brackets that doesn't conflict with + /// to and return that. + /// See osu.Game.Tests.Utils.NamingUtilsTest for concrete examples of behaviour. + /// + /// + /// and are compared in a case-insensitive manner, + /// so this method is safe to use for naming files in a platform-invariant manner. + /// + /// public static string GetNextBestName(IEnumerable existingNames, string desiredName) { - return null; + string pattern = $@"^(?i){Regex.Escape(desiredName)}(?-i)( \((?[1-9][0-9]*)\))?$"; + var regex = new Regex(pattern, RegexOptions.Compiled); + var takenNumbers = new HashSet(); + + foreach (string name in existingNames) + { + var match = regex.Match(name); + if (!match.Success) + continue; + + string copyNumberString = match.Groups[@"copyNumber"].Value; + + if (string.IsNullOrEmpty(copyNumberString)) + { + takenNumbers.Add(0); + continue; + } + + takenNumbers.Add(int.Parse(copyNumberString)); + } + + int bestNumber = 0; + while (takenNumbers.Contains(bestNumber)) + bestNumber += 1; + + return bestNumber == 0 + ? desiredName + : $"{desiredName} ({bestNumber})"; } } } From 8a08bb7aaf88ec33c64c01659cd0ea9c42f93f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Feb 2022 23:12:13 +0100 Subject: [PATCH 579/587] Use best-name-finding helper in new difficulty creation flow --- osu.Game/Beatmaps/BeatmapManager.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 777d5db2ad..5f7de0d762 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -22,6 +22,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Skinning; using osu.Game.Stores; +using osu.Game.Utils; #nullable enable @@ -123,7 +124,10 @@ namespace osu.Game.Beatmaps { var playableBeatmap = referenceWorkingBeatmap.GetPlayableBeatmap(rulesetInfo); - var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), playableBeatmap.Metadata.DeepClone()); + var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), playableBeatmap.Metadata.DeepClone()) + { + DifficultyName = NamingUtils.GetNextBestName(targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName), "New Difficulty") + }; var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; foreach (var timingPoint in playableBeatmap.ControlPointInfo.TimingPoints) newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); @@ -150,8 +154,10 @@ namespace osu.Game.Beatmaps newBeatmap.BeatmapInfo = newBeatmapInfo = referenceWorkingBeatmap.BeatmapInfo.Clone(); // assign a new ID to the clone. newBeatmapInfo.ID = Guid.NewGuid(); - // add "(copy)" suffix to difficulty name to avoid clashes on save. - newBeatmapInfo.DifficultyName += " (copy)"; + // add "(copy)" suffix to difficulty name, and additionally ensure that it doesn't conflict with any other potentially pre-existing copies. + newBeatmapInfo.DifficultyName = NamingUtils.GetNextBestName( + targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName), + $"{newBeatmapInfo.DifficultyName} (copy)"); // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. newBeatmapInfo.Hash = string.Empty; // clear online properties. From e459523afe5cd351e98a8558f41ca933ef3374b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Feb 2022 23:13:43 +0100 Subject: [PATCH 580/587] Adjust beatmap creation test cases to new behaviour --- .../Editing/TestSceneEditorBeatmapCreation.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 0a2f622da1..ecd4035edd 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -269,11 +269,12 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestCreateNewBeatmapFailsWithBlankNamedDifficulties() + public void TestCreateMultipleNewDifficultiesSucceeds() { Guid setId = Guid.Empty; AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = "New Difficulty"); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => { @@ -282,15 +283,24 @@ namespace osu.Game.Tests.Visual.Editing }); AddStep("try to create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); - AddAssert("beatmap set unchanged", () => + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction()); + + AddUntilStep("wait for created", () => + { + string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != "New Difficulty"; + }); + AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)"); + AddAssert("new difficulty persisted", () => { var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); - return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Files.Count == 2); }); } [Test] - public void TestCreateNewBeatmapFailsWithSameNamedDifficulties([Values] bool sameRuleset) + public void TestSavingBeatmapFailsWithSameNamedDifficulties([Values] bool sameRuleset) { Guid setId = Guid.Empty; const string duplicate_difficulty_name = "duplicate"; From aac1c53b060cc1885297baa3fc5709b6a5749097 Mon Sep 17 00:00:00 2001 From: Stedoss <29103029+Stedoss@users.noreply.github.com> Date: Thu, 17 Feb 2022 03:04:16 +0000 Subject: [PATCH 581/587] Remove creator name from playlist item panel beatmap text --- .../Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index dcf2a5a480..7533bfc8b6 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -291,10 +291,14 @@ namespace osu.Game.Screens.OnlinePlay if (Item.Beatmap.Value != null) { - beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineID.ToString(), null, text => - { - text.Truncate = true; - }); + beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(includeCreator: false), + LinkAction.OpenBeatmap, + Item.Beatmap.Value.OnlineID.ToString(), + null, + text => + { + text.Truncate = true; + }); } authorText.Clear(); From 7307e68e9c55bf1be4bde2e94d912e6a9df24f63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Feb 2022 13:26:12 +0900 Subject: [PATCH 582/587] Revert "Merge pull request #16889 from smoogipoo/remove-mod-multiplier" This reverts commit 252b945d3b0cd4e6f33ac334f23bcf119a119c2a, reversing changes made to a1b39a96cfe3f4d0f02652ded5ebca09ba1d24e1. --- .../Difficulty/ManiaPerformanceCalculator.cs | 16 +++++------ .../TestSceneSpinnerRotation.cs | 3 ++- .../TestSceneFooterButtonMods.cs | 27 +++++++++++++++++-- osu.Game/Rulesets/Mods/Mod.cs | 7 ++--- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 13 ++++++++- osu.Game/Screens/Select/FooterButtonMods.cs | 25 +++++++++++++++++ 6 files changed, 74 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index beba29b8bb..8a8c41bb8a 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -43,11 +43,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - if (Attributes.ScoreMultiplier > 0) - { - // Scale score up, so it's comparable to other keymods - scaledScore *= 1.0 / Attributes.ScoreMultiplier; - } + IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease); + + double scoreMultiplier = 1.0; + foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) + scoreMultiplier *= m.ScoreMultiplier; + + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / scoreMultiplier; // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // The specific number has no intrinsic meaning and can be adjusted as needed. @@ -77,9 +80,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeDifficultyValue() { - if (Attributes.ScoreMultiplier <= 0) - return 0; - double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 0b9db8e20a..de795241bf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -147,7 +147,8 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("player score matching expected bonus score", () => { - double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value; + // multipled by 2 to nullify the score multiplier. (autoplay mod selected) + double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index e9014c0941..0631059d1a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; +using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Select; @@ -12,11 +14,11 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneFooterButtonMods : OsuTestScene { - private readonly FooterButtonMods footerButtonMods; + private readonly TestFooterButtonMods footerButtonMods; public TestSceneFooterButtonMods() { - Add(footerButtonMods = new FooterButtonMods()); + Add(footerButtonMods = new TestFooterButtonMods()); } [Test] @@ -24,15 +26,19 @@ namespace osu.Game.Tests.Visual.UserInterface { var hiddenMod = new Mod[] { new OsuModHidden() }; AddStep(@"Add Hidden", () => changeMods(hiddenMod)); + AddAssert(@"Check Hidden multiplier", () => assertModsMultiplier(hiddenMod)); var hardRockMod = new Mod[] { new OsuModHardRock() }; AddStep(@"Add HardRock", () => changeMods(hardRockMod)); + AddAssert(@"Check HardRock multiplier", () => assertModsMultiplier(hardRockMod)); var doubleTimeMod = new Mod[] { new OsuModDoubleTime() }; AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod)); + AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod)); var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods)); } [Test] @@ -40,12 +46,15 @@ namespace osu.Game.Tests.Visual.UserInterface { var easyMod = new Mod[] { new OsuModEasy() }; AddStep(@"Add Easy", () => changeMods(easyMod)); + AddAssert(@"Check Easy multiplier", () => assertModsMultiplier(easyMod)); var noFailMod = new Mod[] { new OsuModNoFail() }; AddStep(@"Add NoFail", () => changeMods(noFailMod)); + AddAssert(@"Check NoFail multiplier", () => assertModsMultiplier(noFailMod)); var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() }; AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleDecrementMods)); } [Test] @@ -54,11 +63,25 @@ namespace osu.Game.Tests.Visual.UserInterface var multipleMods = new Mod[] { new OsuModDoubleTime(), new OsuModFlashlight() }; AddStep(@"Add mods", () => changeMods(multipleMods)); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); + AddAssert(@"Check empty multiplier", () => assertModsMultiplier(Array.Empty())); } private void changeMods(IReadOnlyList mods) { footerButtonMods.Current.Value = mods; } + + private bool assertModsMultiplier(IEnumerable mods) + { + double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + string expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; + + return expectedValue == footerButtonMods.MultiplierText.Current.Value; + } + + private class TestFooterButtonMods : FooterButtonMods + { + public new OsuSpriteText MultiplierText => base.MultiplierText; + } } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 9a45e2458d..7136795461 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -80,13 +80,10 @@ namespace osu.Game.Rulesets.Mods } /// - /// The (legacy) score multiplier of this mod. + /// The score multiplier of this mod. /// - /// - /// This is not applied for newly set scores, but may be required for display purposes when showing legacy scores. - /// [JsonIgnore] - public virtual double ScoreMultiplier => 1; + public abstract double ScoreMultiplier { get; } /// /// Returns true if this mod is implemented (and playable). diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index ce1486b02f..79861c0ecc 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -92,6 +92,8 @@ namespace osu.Game.Rulesets.Scoring private readonly List hitEvents = new List(); private HitObject lastHitObject; + private double scoreMultiplier = 1; + public ScoreProcessor() { accuracyPortion = DefaultAccuracyPortion; @@ -109,6 +111,15 @@ namespace osu.Game.Rulesets.Scoring }; Mode.ValueChanged += _ => updateScore(); + Mods.ValueChanged += mods => + { + scoreMultiplier = 1; + + foreach (var m in mods.NewValue) + scoreMultiplier *= m.ScoreMultiplier; + + updateScore(); + }; } private readonly Dictionary scoreResultCounts = new Dictionary(); @@ -224,7 +235,7 @@ namespace osu.Game.Rulesets.Scoring case ScoringMode.Standardised: double accuracyScore = accuracyPortion * accuracyRatio; double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)); + return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier; case ScoringMode.Classic: // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 1f1aa4c4b3..5bbca5ca1a 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -6,11 +6,14 @@ using osu.Framework.Graphics; using osu.Game.Screens.Play.HUD; using osu.Game.Rulesets.Mods; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osuTK; +using osuTK.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Screens.Select @@ -23,7 +26,10 @@ namespace osu.Game.Screens.Select set => modDisplay.Current = value; } + protected readonly OsuSpriteText MultiplierText; private readonly ModDisplay modDisplay; + private Color4 lowMultiplierColour; + private Color4 highMultiplierColour; public FooterButtonMods() { @@ -34,6 +40,12 @@ namespace osu.Game.Screens.Select Scale = new Vector2(0.8f), ExpansionMode = ExpansionMode.AlwaysContracted, }); + ButtonContentContainer.Add(MultiplierText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(weight: FontWeight.Bold), + }); } [BackgroundDependencyLoader] @@ -41,6 +53,8 @@ namespace osu.Game.Screens.Select { SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); + lowMultiplierColour = colours.Red; + highMultiplierColour = colours.Green; Text = @"mods"; Hotkey = GlobalAction.ToggleModSelection; } @@ -54,6 +68,17 @@ namespace osu.Game.Screens.Select private void updateMultiplierText() { + double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; + + MultiplierText.Text = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; + + if (multiplier > 1.0) + MultiplierText.FadeColour(highMultiplierColour, 200); + else if (multiplier < 1.0) + MultiplierText.FadeColour(lowMultiplierColour, 200); + else + MultiplierText.FadeColour(Color4.White, 200); + if (Current.Value?.Count > 0) modDisplay.FadeIn(); else From 39a7bbdb9ad0cd23ccffc45b0760b228a078393d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Feb 2022 14:03:38 +0900 Subject: [PATCH 583/587] Fix mania PP calculator applying incorrect score multiplier --- .../Difficulty/ManiaPerformanceCalculator.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 8a8c41bb8a..722cb55036 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -43,14 +43,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease); - - double scoreMultiplier = 1.0; - foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) - scoreMultiplier *= m.ScoreMultiplier; - - // Scale score up, so it's comparable to other keymods - scaledScore *= 1.0 / scoreMultiplier; + if (Attributes.ScoreMultiplier > 0) + { + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / Attributes.ScoreMultiplier; + } // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // The specific number has no intrinsic meaning and can be adjusted as needed. From dc74d17478605d1fbb03b434623a49a2f72970b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Feb 2022 16:45:20 +0900 Subject: [PATCH 584/587] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index eab2be1b72..24a0d20874 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7b50c804ff..4c6f81defa 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 03a105673c..99b9de3fe2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From 3d5ed24e206a43990f36880464917045e8cdf2be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Feb 2022 21:04:59 +0900 Subject: [PATCH 585/587] Fix beatmap overlay leaderboards and links not working Completely aware that this isn't how it should be done, but would like to get this out in a hotfix release today. Maybe changes opinions on https://github.com/ppy/osu/pull/16890 structure? --- .../API/Requests/Responses/APIBeatmap.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index dca60e54cb..c53fab48ae 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -112,7 +112,27 @@ namespace osu.Game.Online.API.Requests.Responses public int OnlineID { get; set; } = -1; public string Name => $@"{nameof(APIRuleset)} (ID: {OnlineID})"; - public string ShortName => nameof(APIRuleset); + + public string ShortName + { + get + { + // TODO: this should really not exist. + switch (OnlineID) + { + case 0: return "osu"; + + case 1: return "taiko"; + + case 2: return "catch"; + + case 3: return "fruits"; + + default: throw new ArgumentOutOfRangeException(); + } + } + } + public string InstantiationInfo => string.Empty; public Ruleset CreateInstance() => throw new NotImplementedException(); From 9d0023c750a85c2cc78a6d6d5004f50231916a92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Feb 2022 21:12:51 +0900 Subject: [PATCH 586/587] Fix incorrect mappings --- osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index c53fab48ae..f5795141c5 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -124,9 +124,9 @@ namespace osu.Game.Online.API.Requests.Responses case 1: return "taiko"; - case 2: return "catch"; + case 2: return "fruits"; - case 3: return "fruits"; + case 3: return "mania"; default: throw new ArgumentOutOfRangeException(); } From 08317b4265c94a6f6b30a6629d002175a7945309 Mon Sep 17 00:00:00 2001 From: OctopuSSX Date: Thu, 17 Feb 2022 20:43:36 +0300 Subject: [PATCH 587/587] Update ScreenshotManager.cs --- osu.Game/Graphics/ScreenshotManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index a39d7bfb47..b0f20de685 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -112,6 +112,8 @@ namespace osu.Game.Graphics if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false) cursorVisibility.Value = true; + host.GetClipboard()?.SetImage(image); + string filename = getFilename(); if (filename == null) return;