1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-26 13:50:33 +08:00

Compare commits

..

52 Commits

80 changed files with 794 additions and 195 deletions
@@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
public TestCatcher(DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty)
: base(droppedObjectTarget, difficulty)
{
}
@@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public class TestKiaiFruit : Fruit
{
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private ScheduledDelegate addManyFruit;
private BeatmapDifficulty beatmapDifficulty;
private IBeatmapDifficultyInfo beatmapDifficulty;
public TestSceneCatcherArea()
{
@@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea
{
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
public TestCatcherArea(IBeatmapDifficultyInfo beatmapDifficulty)
{
var droppedObjectContainer = new DroppedObjectContainer();
Add(droppedObjectContainer);
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return new CatchDifficultyAttributes { Mods = mods, Skills = skills };
// this is the same as osu!, so there's potential to share the implementation... maybe
double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
return new CatchDifficultyAttributes
{
@@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Edit
public class CatchEditorPlayfield : CatchPlayfield
{
// TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
public CatchEditorPlayfield(BeatmapDifficulty difficulty)
public CatchEditorPlayfield(IBeatmapDifficultyInfo difficulty)
: base(difficulty)
{
}
@@ -128,11 +128,11 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary>
public int RandomSeed => (int)StartTime;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
}
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary>
public double SpanDuration => Duration / this.SpanCount();
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+2 -2
View File
@@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Catch.UI
internal CatcherArea CatcherArea { get; private set; }
private readonly BeatmapDifficulty difficulty;
private readonly IBeatmapDifficultyInfo difficulty;
public CatchPlayfield(BeatmapDifficulty difficulty)
public CatchPlayfield(IBeatmapDifficultyInfo difficulty)
{
this.difficulty = difficulty;
}
+3 -3
View File
@@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool<CaughtBanana> caughtBananaPool;
private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null)
public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty = null)
{
this.droppedObjectTarget = droppedObjectTarget;
@@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary>
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
/// </summary>
private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
/// <summary>
/// Calculates the width of the area used for attempting catches in gameplay.
@@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// Calculates the width of the area used for attempting catches in gameplay.
/// </summary>
/// <param name="difficulty">The beatmap difficulty.</param>
public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
public static float CalculateCatchWidth(IBeatmapDifficultyInfo difficulty) => CalculateCatchWidth(calculateScale(difficulty));
/// <summary>
/// Determine if this catcher can catch a <see cref="CatchHitObject"/> in the current position.
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.UI
: base(ruleset, beatmap, mods)
{
Direction.Value = ScrollingDirection.Down;
TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
}
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
@@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
IBeatmapDifficultyInfo difficulty = original.BeatmapInfo.BaseDifficulty;
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new FastRandom(seed);
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (drainTime == 0)
drainTime = 10000;
BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
IBeatmapDifficultyInfo difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
+1 -1
View File
@@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary>
private double tickSpacing = 50;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
public class CheckTooShortSpinnersTest
{
private CheckTooShortSpinners check;
private BeatmapDifficulty difficulty;
private IBeatmapDifficultyInfo difficulty;
[SetUp]
public void Setup()
@@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
assertTooShort(new List<HitObject> { spinnerHighOd }, difficultyHighOd);
}
private void assertOk(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
private void assertOk(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{
Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty);
}
private void assertVeryShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
private void assertVeryShort(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort);
}
private void assertTooShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
private void assertTooShort(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
@@ -102,12 +102,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort);
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = hitObjects,
BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty }
BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(beatmapDifficulty) }
};
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
@@ -452,7 +452,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestSpinner : Spinner
{
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
SpinsRequired = 1;
@@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestSpinner : Spinner
{
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
SpinsRequired = 1;
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
+1 -1
View File
@@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Mods
#region Reduce AR (IApplicableToDifficulty)
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty)
{
}
@@ -122,11 +122,11 @@ namespace osu.Game.Rulesets.Osu.Objects
});
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
// Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.
+1 -1
View File
@@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Objects
Path.Version.ValueChanged += _ => updateNestedPositions();
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public double SpanDuration => slider.SpanDuration;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+1 -1
View File
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public int SpanIndex { get; set; }
public double SpanStartTime { get; set; }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+2 -2
View File
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public int MaximumBonusSpins { get; protected set; } = 1;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects
double secondsDuration = Duration / 1000;
double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
@@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
case IHasDuration endTimeData:
{
double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
yield return new Swell
{
@@ -193,9 +193,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty
{
public TaikoMutliplierAppliedDifficulty(BeatmapDifficulty difficulty)
public TaikoMutliplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
{
difficulty.CopyTo(this);
CopyFrom(difficulty);
SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{
base.ApplyBeatmap(beatmap);
hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType<Hit>().Count()) * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98));
hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120);
hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType<Hit>().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98));
hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120);
}
protected override double GetHealthIncreaseFor(JudgementResult result)
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Localisation
Title = "Romanised title",
TitleUnicode = "Unicode Title"
};
var romanisableString = metadata.ToRomanisableString();
var romanisableString = metadata.GetDisplayTitleRomanisable();
Assert.AreEqual(metadata.ToString(), romanisableString.Romanised);
Assert.AreEqual($"{metadata.ArtistUnicode} - {metadata.TitleUnicode}", romanisableString.Original);
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.Localisation
Artist = "Romanised Artist",
Title = "Romanised title"
};
var romanisableString = metadata.ToRomanisableString();
var romanisableString = metadata.GetDisplayTitleRomanisable();
Assert.AreEqual(romanisableString.Romanised, romanisableString.Original);
}
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Online
AddAssert("response event fired", () => response != null);
AddAssert("request has response", () => request.Result == response);
AddAssert("request has response", () => request.Response == response);
}
[Test]
@@ -0,0 +1,62 @@
// 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.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorSaving : OsuGameTestScene
{
private Editor editor => Game.ChildrenOfType<Editor>().FirstOrDefault();
private EditorBeatmap editorBeatmap => (EditorBeatmap)editor.Dependencies.Get(typeof(EditorBeatmap));
/// <summary>
/// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select.
/// </summary>
[Test]
public void TestNewBeatmapSaveThenLoad()
{
AddStep("set default beatmap", () => Game.Beatmap.SetDefault());
PushAndConfirm(() => new EditorLoader());
AddUntilStep("wait for editor load", () => editor != null);
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("Enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true);
AddStep("Change to placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left));
AddStep("Save and exit", () =>
{
InputManager.Keys(PlatformAction.Save);
InputManager.Key(Key.Escape);
});
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
PushAndConfirm(() => new PlaySongSelect());
AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault);
AddStep("Open options", () => InputManager.Key(Key.F3));
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
AddUntilStep("Wait for editor load", () => editor != null);
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
}
}
}
@@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -68,6 +69,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
LoadScreen(dependenciesScreen = new DependenciesScreen(client));
});
AddUntilStep("wait for dependencies screen", () => Stack.CurrentScreen is DependenciesScreen);
AddUntilStep("wait for dependencies to start load", () => dependenciesScreen.LoadState > LoadState.NotLoaded);
AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded);
AddStep("load multiplayer", () => LoadScreen(multiplayerScreen));
@@ -221,6 +221,8 @@ namespace osu.Game.Tests.Visual.Ranking
list.SelectedScore.Value = middleScore;
});
AddUntilStep("wait for all scores to be visible", () => list.ChildrenOfType<ScorePanelTrackingContainer>().All(t => t.IsPresent));
assertScoreState(highestScore, false);
assertScoreState(middleScore, true);
assertScoreState(lowestScore, false);
@@ -714,10 +714,11 @@ namespace osu.Game.Tests.Visual.SongSelect
});
FilterableDifficultyIcon difficultyIcon = null;
AddStep("Find an icon for different ruleset", () =>
AddUntilStep("Find an icon for different ruleset", () =>
{
difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
.First(icon => icon.Item.BeatmapInfo.Ruleset.ID == 3);
.FirstOrDefault(icon => icon.Item.BeatmapInfo.Ruleset.ID == 3);
return difficultyIcon != null;
});
AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0);
@@ -162,6 +162,8 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
}
@@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.UserInterface
var req = new GetBeatmapSetRequest(1);
api.Queue(req);
AddUntilStep("wait for api response", () => req.Result != null);
AddUntilStep("wait for api response", () => req.Response != null);
TestUpdateableBeatmapBackgroundSprite background = null;
@@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Child = background = new TestUpdateableBeatmapBackgroundSprite
{
RelativeSizeAxes = Axes.Both,
Beatmap = { Value = new BeatmapInfo { BeatmapSet = req.Result?.ToBeatmapSet(rulesets) } }
Beatmap = { Value = new BeatmapInfo { BeatmapSet = req.Response?.ToBeatmapSet(rulesets) } }
};
});
+1 -1
View File
@@ -117,7 +117,7 @@ namespace osu.Game.Tournament.Components
if ((mods & LegacyMods.DoubleTime) > 0)
{
// temporary local calculation (taken from OsuDifficultyCalculator)
double preempt = (int)BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450) / 1.5;
double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(ar, 1800, 1200, 450) / 1.5;
ar = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5);
bpm *= 1.5f;
+2 -2
View File
@@ -182,7 +182,7 @@ namespace osu.Game.Tournament
{
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
API.Perform(req);
b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore);
b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore);
addedInfo = true;
}
@@ -203,7 +203,7 @@ namespace osu.Game.Tournament
{
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
req.Perform(API);
b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore);
b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore);
addedInfo = true;
}
+21 -43
View File
@@ -5,7 +5,7 @@ using osu.Game.Database;
namespace osu.Game.Beatmaps
{
public class BeatmapDifficulty : IHasPrimaryKey
public class BeatmapDifficulty : IHasPrimaryKey, IBeatmapDifficultyInfo
{
/// <summary>
/// The default value used for all difficulty settings except <see cref="SliderMultiplier"/> and <see cref="SliderTickRate"/>.
@@ -20,6 +20,15 @@ namespace osu.Game.Beatmaps
private float? approachRate;
public BeatmapDifficulty()
{
}
public BeatmapDifficulty(IBeatmapDifficultyInfo source)
{
CopyFrom(source);
}
public float ApproachRate
{
get => approachRate ?? OverallDifficulty;
@@ -39,6 +48,17 @@ namespace osu.Game.Beatmaps
return diff;
}
public void CopyFrom(IBeatmapDifficultyInfo difficulty)
{
ApproachRate = difficulty.ApproachRate;
DrainRate = difficulty.DrainRate;
CircleSize = difficulty.CircleSize;
OverallDifficulty = difficulty.OverallDifficulty;
SliderMultiplier = difficulty.SliderMultiplier;
SliderTickRate = difficulty.SliderTickRate;
}
public void CopyTo(BeatmapDifficulty difficulty)
{
difficulty.ApproachRate = ApproachRate;
@@ -49,47 +69,5 @@ namespace osu.Game.Beatmaps
difficulty.SliderMultiplier = SliderMultiplier;
difficulty.SliderTickRate = SliderTickRate;
}
/// <summary>
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
/// </summary>
/// <param name="difficulty">The difficulty value to be mapped.</param>
/// <param name="min">Minimum of the resulting range which will be achieved by a difficulty value of 0.</param>
/// <param name="mid">Midpoint of the resulting range which will be achieved by a difficulty value of 5.</param>
/// <param name="max">Maximum of the resulting range which will be achieved by a difficulty value of 10.</param>
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
public static double DifficultyRange(double difficulty, double min, double mid, double max)
{
if (difficulty > 5)
return mid + (max - mid) * (difficulty - 5) / 5;
if (difficulty < 5)
return mid - (mid - min) * (5 - difficulty) / 5;
return mid;
}
/// <summary>
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
/// </summary>
/// <param name="difficulty">The difficulty value to be mapped.</param>
/// <param name="range">The values that define the two linear ranges.
/// <list type="table">
/// <item>
/// <term>od0</term>
/// <description>Minimum of the resulting range which will be achieved by a difficulty value of 0.</description>
/// </item>
/// <item>
/// <term>od5</term>
/// <description>Midpoint of the resulting range which will be achieved by a difficulty value of 5.</description>
/// </item>
/// <item>
/// <term>od10</term>
/// <description>Maximum of the resulting range which will be achieved by a difficulty value of 10.</description>
/// </item>
/// </list>
/// </param>
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
public static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range)
=> DifficultyRange(difficulty, range.od0, range.od5, range.od10);
}
}
+19 -14
View File
@@ -7,7 +7,6 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Rulesets;
@@ -17,7 +16,7 @@ namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
[Serializable]
public class BeatmapInfo : IEquatable<BeatmapInfo>, IHasPrimaryKey
public class BeatmapInfo : IEquatable<BeatmapInfo>, IHasPrimaryKey, IBeatmapInfo
{
public int ID { get; set; }
@@ -152,18 +151,7 @@ namespace osu.Game.Beatmaps
[JsonIgnore]
public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarDifficulty);
public string[] SearchableTerms => new[]
{
Version
}.Concat(Metadata?.SearchableTerms ?? Enumerable.Empty<string>()).Where(s => !string.IsNullOrEmpty(s)).ToArray();
public override string ToString() => $"{Metadata ?? BeatmapSet?.Metadata} {versionString}".Trim();
public RomanisableString ToRomanisableString()
{
var metadata = (Metadata ?? BeatmapSet?.Metadata)?.ToRomanisableString() ?? new RomanisableString(null, null);
return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim());
}
public override string ToString() => this.GetDisplayTitle();
public bool Equals(BeatmapInfo other)
{
@@ -187,5 +175,22 @@ namespace osu.Game.Beatmaps
/// Returns a shallow-clone of this <see cref="BeatmapInfo"/>.
/// </summary>
public BeatmapInfo Clone() => (BeatmapInfo)MemberwiseClone();
#region Implementation of IHasOnlineID
public int? OnlineID => OnlineBeatmapID;
#endregion
#region Implementation of IBeatmapInfo
string IBeatmapInfo.DifficultyName => Version;
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => BaseDifficulty;
IBeatmapSetInfo IBeatmapInfo.BeatmapSet => BeatmapSet;
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
double IBeatmapInfo.StarRating => StarDifficulty;
#endregion
}
}
@@ -0,0 +1,38 @@
// 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.
using System.Linq;
using osu.Framework.Localisation;
namespace osu.Game.Beatmaps
{
public static class BeatmapInfoExtensions
{
/// <summary>
/// A user-presentable display title representing this beatmap.
/// </summary>
public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{getClosestMetadata(beatmapInfo)} {getVersionString(beatmapInfo)}".Trim();
/// <summary>
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.
/// </summary>
public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo)
{
var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable();
var versionString = getVersionString(beatmapInfo);
return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim());
}
public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[]
{
beatmapInfo.DifficultyName
}.Concat(getClosestMetadata(beatmapInfo).GetSearchableTerms()).Where(s => !string.IsNullOrEmpty(s)).ToArray();
private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]";
// temporary helper methods until we figure which metadata should be where.
private static IBeatmapMetadataInfo getClosestMetadata(IBeatmapInfo beatmapInfo) =>
beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet?.Metadata ?? new BeatmapMetadata();
}
}
+4
View File
@@ -45,6 +45,7 @@ namespace osu.Game.Beatmaps
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host);
workingBeatmapCache.BeatmapManager = beatmapModelManager;
beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache;
if (performOnlineLookups)
{
@@ -305,6 +306,9 @@ namespace osu.Game.Beatmaps
public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap);
void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo);
void IWorkingBeatmapCache.Invalidate(BeatmapInfo beatmapInfo) => workingBeatmapCache.Invalidate(beatmapInfo);
#endregion
#region Implementation of IModelFileManager<in BeatmapSetInfo,in BeatmapSetFileInfo>
+5 -44
View File
@@ -4,9 +4,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Users;
@@ -15,7 +13,7 @@ namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
[Serializable]
public class BeatmapMetadata : IEquatable<BeatmapMetadata>, IHasPrimaryKey
public class BeatmapMetadata : IEquatable<BeatmapMetadata>, IHasPrimaryKey, IBeatmapMetadataInfo
{
public int ID { get; set; }
@@ -83,50 +81,13 @@ namespace osu.Game.Beatmaps
public int PreviewTime { get; set; }
public string AudioFile { get; set; }
public string BackgroundFile { get; set; }
public override string ToString()
{
string author = Author == null ? string.Empty : $"({Author})";
return $"{Artist} - {Title} {author}".Trim();
}
public bool Equals(BeatmapMetadata other) => ((IBeatmapMetadataInfo)this).Equals(other);
public RomanisableString ToRomanisableString()
{
string author = Author == null ? string.Empty : $"({Author})";
var artistUnicode = string.IsNullOrEmpty(ArtistUnicode) ? Artist : ArtistUnicode;
var titleUnicode = string.IsNullOrEmpty(TitleUnicode) ? Title : TitleUnicode;
public override string ToString() => this.GetDisplayTitle();
return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{Artist} - {Title} {author}".Trim());
}
[JsonIgnore]
public string[] SearchableTerms => new[]
{
Author?.Username,
Artist,
ArtistUnicode,
Title,
TitleUnicode,
Source,
Tags
}.Where(s => !string.IsNullOrEmpty(s)).ToArray();
public bool Equals(BeatmapMetadata other)
{
if (other == null)
return false;
return Title == other.Title
&& TitleUnicode == other.TitleUnicode
&& Artist == other.Artist
&& ArtistUnicode == other.ArtistUnicode
&& AuthorString == other.AuthorString
&& Source == other.Source
&& Tags == other.Tags
&& PreviewTime == other.PreviewTime
&& AudioFile == other.AudioFile
&& BackgroundFile == other.BackgroundFile;
}
string IBeatmapMetadataInfo.Author => AuthorString;
}
}
@@ -0,0 +1,46 @@
// 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.
using System.Linq;
using osu.Framework.Localisation;
namespace osu.Game.Beatmaps
{
public static class BeatmapMetadataInfoExtensions
{
/// <summary>
/// An array of all searchable terms provided in contained metadata.
/// </summary>
public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo) => new[]
{
metadataInfo.Author,
metadataInfo.Artist,
metadataInfo.ArtistUnicode,
metadataInfo.Title,
metadataInfo.TitleUnicode,
metadataInfo.Source,
metadataInfo.Tags
}.Where(s => !string.IsNullOrEmpty(s)).ToArray();
/// <summary>
/// A user-presentable display title representing this metadata.
/// </summary>
public static string GetDisplayTitle(this IBeatmapMetadataInfo metadataInfo)
{
string author = string.IsNullOrEmpty(metadataInfo.Author) ? string.Empty : $"({metadataInfo.Author})";
return $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim();
}
/// <summary>
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.
/// </summary>
public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapMetadataInfo metadataInfo)
{
string author = string.IsNullOrEmpty(metadataInfo.Author) ? string.Empty : $"({metadataInfo.Author})";
var artistUnicode = string.IsNullOrEmpty(metadataInfo.ArtistUnicode) ? metadataInfo.Artist : metadataInfo.ArtistUnicode;
var titleUnicode = string.IsNullOrEmpty(metadataInfo.TitleUnicode) ? metadataInfo.Title : metadataInfo.TitleUnicode;
return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim());
}
}
}
+2 -2
View File
@@ -32,7 +32,7 @@ namespace osu.Game.Beatmaps
/// Handles ef-core storage of beatmaps.
/// </summary>
[ExcludeFromDynamicCompile]
public class BeatmapModelManager : ArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>
public class BeatmapModelManager : ArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IBeatmapModelManager
{
/// <summary>
/// Fired when a single difficulty has been hidden.
@@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// The game working beatmap cache, used to invalidate entries on changes.
/// </summary>
public WorkingBeatmapCache WorkingBeatmapCache { private get; set; }
public IWorkingBeatmapCache WorkingBeatmapCache { private get; set; }
private readonly Bindable<WeakReference<BeatmapInfo>> beatmapRestored = new Bindable<WeakReference<BeatmapInfo>>();
@@ -78,7 +78,7 @@ namespace osu.Game.Beatmaps
// intentionally blocking to limit web request concurrency
api.Perform(req);
var res = req.Result;
var res = req.Response;
if (res != null)
{
+3 -1
View File
@@ -7,7 +7,7 @@ using osu.Game.IO;
namespace osu.Game.Beatmaps
{
public class BeatmapSetFileInfo : INamedFileInfo, IHasPrimaryKey
public class BeatmapSetFileInfo : INamedFileInfo, IHasPrimaryKey, INamedFileUsage
{
public int ID { get; set; }
@@ -19,5 +19,7 @@ namespace osu.Game.Beatmaps
[Required]
public string Filename { get; set; }
public IFileInfo File => FileInfo;
}
}
+15 -3
View File
@@ -12,7 +12,7 @@ using osu.Game.Database;
namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete, IEquatable<BeatmapSetInfo>
public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete, IEquatable<BeatmapSetInfo>, IBeatmapSetInfo
{
public int ID { get; set; }
@@ -61,8 +61,6 @@ namespace osu.Game.Beatmaps
public string Hash { get; set; }
public string StoryboardFile => Files.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename;
/// <summary>
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
@@ -90,5 +88,19 @@ namespace osu.Game.Beatmaps
return ReferenceEquals(this, other);
}
#region Implementation of IHasOnlineID
public int? OnlineID => OnlineBeatmapSetID;
#endregion
#region Implementation of IBeatmapSetInfo
IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => Metadata;
IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps;
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => Files;
#endregion
}
}
@@ -0,0 +1,90 @@
// 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.
#nullable enable
namespace osu.Game.Beatmaps
{
/// <summary>
/// A representation of all top-level difficulty settings for a beatmap.
/// </summary>
public interface IBeatmapDifficultyInfo
{
/// <summary>
/// The default value used for all difficulty settings except <see cref="SliderMultiplier"/> and <see cref="SliderTickRate"/>.
/// </summary>
const float DEFAULT_DIFFICULTY = 5;
/// <summary>
/// The drain rate of the associated beatmap.
/// </summary>
float DrainRate { get; }
/// <summary>
/// The circle size of the associated beatmap.
/// </summary>
float CircleSize { get; }
/// <summary>
/// The overall difficulty of the associated beatmap.
/// </summary>
float OverallDifficulty { get; }
/// <summary>
/// The approach rate of the associated beatmap.
/// </summary>
float ApproachRate { get; }
/// <summary>
/// The slider multiplier of the associated beatmap.
/// </summary>
double SliderMultiplier { get; }
/// <summary>
/// The slider tick rate of the associated beatmap.
/// </summary>
double SliderTickRate { get; }
/// <summary>
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
/// </summary>
/// <param name="difficulty">The difficulty value to be mapped.</param>
/// <param name="min">Minimum of the resulting range which will be achieved by a difficulty value of 0.</param>
/// <param name="mid">Midpoint of the resulting range which will be achieved by a difficulty value of 5.</param>
/// <param name="max">Maximum of the resulting range which will be achieved by a difficulty value of 10.</param>
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
static double DifficultyRange(double difficulty, double min, double mid, double max)
{
if (difficulty > 5)
return mid + (max - mid) * (difficulty - 5) / 5;
if (difficulty < 5)
return mid - (mid - min) * (5 - difficulty) / 5;
return mid;
}
/// <summary>
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
/// </summary>
/// <param name="difficulty">The difficulty value to be mapped.</param>
/// <param name="range">The values that define the two linear ranges.
/// <list type="table">
/// <item>
/// <term>od0</term>
/// <description>Minimum of the resulting range which will be achieved by a difficulty value of 0.</description>
/// </item>
/// <item>
/// <term>od5</term>
/// <description>Midpoint of the resulting range which will be achieved by a difficulty value of 5.</description>
/// </item>
/// <item>
/// <term>od10</term>
/// <description>Maximum of the resulting range which will be achieved by a difficulty value of 10.</description>
/// </item>
/// </list>
/// </param>
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range)
=> DifficultyRange(difficulty, range.od0, range.od5, range.od10);
}
}
+66
View File
@@ -0,0 +1,66 @@
// 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.
using osu.Game.Database;
using osu.Game.Rulesets;
#nullable enable
namespace osu.Game.Beatmaps
{
/// <summary>
/// A single beatmap difficulty.
/// </summary>
public interface IBeatmapInfo : IHasOnlineID
{
/// <summary>
/// The user-specified name given to this beatmap.
/// </summary>
string DifficultyName { get; }
/// <summary>
/// The metadata representing this beatmap. May be shared between multiple beatmaps.
/// </summary>
IBeatmapMetadataInfo? Metadata { get; }
/// <summary>
/// The difficulty settings for this beatmap.
/// </summary>
IBeatmapDifficultyInfo Difficulty { get; }
/// <summary>
/// The beatmap set this beatmap is part of.
/// </summary>
IBeatmapSetInfo? BeatmapSet { get; }
/// <summary>
/// The playable length in milliseconds of this beatmap.
/// </summary>
double Length { get; }
/// <summary>
/// The most common BPM of this beatmap.
/// </summary>
double BPM { get; }
/// <summary>
/// The SHA-256 hash representing this beatmap's contents.
/// </summary>
string Hash { get; }
/// <summary>
/// MD5 is kept for legacy support (matching against replays etc.).
/// </summary>
string MD5Hash { get; }
/// <summary>
/// The ruleset this beatmap was made for.
/// </summary>
IRulesetInfo Ruleset { get; }
/// <summary>
/// The basic star rating for this beatmap (with no mods applied).
/// </summary>
double StarRating { get; }
}
}
+83
View File
@@ -0,0 +1,83 @@
// 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.
using System;
#nullable enable
namespace osu.Game.Beatmaps
{
/// <summary>
/// Metadata representing a beatmap. May be shared between multiple beatmap difficulties.
/// </summary>
public interface IBeatmapMetadataInfo : IEquatable<IBeatmapMetadataInfo>
{
/// <summary>
/// The romanised title of this beatmap.
/// </summary>
string Title { get; }
/// <summary>
/// The unicode title of this beatmap.
/// </summary>
string TitleUnicode { get; }
/// <summary>
/// The romanised artist of this beatmap.
/// </summary>
string Artist { get; }
/// <summary>
/// The unicode artist of this beatmap.
/// </summary>
string ArtistUnicode { get; }
/// <summary>
/// The author of this beatmap.
/// </summary>
string Author { get; } // eventually should be linked to a persisted User.
/// <summary>
/// The source of this beatmap.
/// </summary>
string Source { get; }
/// <summary>
/// The tags of this beatmap.
/// </summary>
string Tags { get; }
/// <summary>
/// The time in milliseconds to begin playing the track for preview purposes.
/// If -1, the track should begin playing at 40% of its length.
/// </summary>
int PreviewTime { get; }
/// <summary>
/// The filename of the audio file consumed by this beatmap.
/// </summary>
string AudioFile { get; }
/// <summary>
/// The filename of the background image file consumed by this beatmap.
/// </summary>
string BackgroundFile { get; }
bool IEquatable<IBeatmapMetadataInfo>.Equals(IBeatmapMetadataInfo? other)
{
if (other == null)
return false;
return Title == other.Title
&& TitleUnicode == other.TitleUnicode
&& Artist == other.Artist
&& ArtistUnicode == other.ArtistUnicode
&& Author == other.Author
&& Source == other.Source
&& Tags == other.Tags
&& PreviewTime == other.PreviewTime
&& AudioFile == other.AudioFile
&& BackgroundFile == other.BackgroundFile;
}
}
}
+20
View File
@@ -0,0 +1,20 @@
// 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.
using osu.Game.Database;
namespace osu.Game.Beatmaps
{
public interface IBeatmapModelManager : IModelManager<BeatmapSetInfo>
{
/// <summary>
/// Provide an online lookup queue component to handle populating online beatmap metadata.
/// </summary>
BeatmapOnlineLookupQueue OnlineLookupQueue { set; }
/// <summary>
/// Provide a working beatmap cache, used to invalidate entries on changes.
/// </summary>
IWorkingBeatmapCache WorkingBeatmapCache { set; }
}
}
+52
View File
@@ -0,0 +1,52 @@
// 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.
using System;
using System.Collections.Generic;
using osu.Game.Database;
#nullable enable
namespace osu.Game.Beatmaps
{
/// <summary>
/// A representation of a collection of beatmap difficulties, generally packaged as an ".osz" archive.
/// </summary>
public interface IBeatmapSetInfo : IHasOnlineID
{
/// <summary>
/// The date when this beatmap was imported.
/// </summary>
DateTimeOffset DateAdded { get; }
/// <summary>
/// The best-effort metadata representing this set. In the case metadata differs between contained beatmaps, one is arbitrarily chosen.
/// </summary>
IBeatmapMetadataInfo? Metadata { get; }
/// <summary>
/// All beatmaps contained in this set.
/// </summary>
IEnumerable<IBeatmapInfo> Beatmaps { get; }
/// <summary>
/// All files used by this set.
/// </summary>
IEnumerable<INamedFileUsage> Files { get; }
/// <summary>
/// The maximum star difficulty of all beatmaps in this set.
/// </summary>
double MaxStarDifficulty { get; }
/// <summary>
/// The maximum playable length in milliseconds of all beatmaps in this set.
/// </summary>
double MaxLength { get; }
/// <summary>
/// The maximum BPM of all beatmaps in this set.
/// </summary>
double MaxBPM { get; }
}
}
+12
View File
@@ -11,5 +11,17 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapInfo">The beatmap to lookup.</param>
/// <returns>A <see cref="WorkingBeatmap"/> instance correlating to the provided <see cref="BeatmapInfo"/>.</returns>
WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo);
/// <summary>
/// Invalidate a cache entry if it exists.
/// </summary>
/// <param name="beatmapSetInfo">The beatmap set info to invalidate any cached entries for.</param>
void Invalidate(BeatmapSetInfo beatmapSetInfo);
/// <summary>
/// Invalidate a cache entry if it exists.
/// </summary>
/// <param name="beatmapInfo">The beatmap info to invalidate any cached entries for.</param>
void Invalidate(BeatmapInfo beatmapInfo);
}
}
+4 -2
View File
@@ -201,12 +201,14 @@ namespace osu.Game.Beatmaps
{
var decoder = Decoder.GetDecoder<Storyboard>(stream);
var storyboardFilename = BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename;
// todo: support loading from both set-wide storyboard *and* beatmap specific.
if (BeatmapSetInfo?.StoryboardFile == null)
if (string.IsNullOrEmpty(storyboardFilename))
storyboard = decoder.Decode(stream);
else
{
using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile))))
using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(storyboardFilename))))
storyboard = decoder.Decode(stream, secondaryStream);
}
}
+15
View File
@@ -0,0 +1,15 @@
// 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.
#nullable enable
namespace osu.Game.Database
{
public interface IHasOnlineID
{
/// <summary>
/// The server-side ID representing this instance, if one exists.
/// </summary>
int? OnlineID { get; }
}
}
+25
View File
@@ -0,0 +1,25 @@
// 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.
using osu.Game.IO;
#nullable enable
namespace osu.Game.Database
{
/// <summary>
/// A usage of a file, with a local filename attached.
/// </summary>
public interface INamedFileUsage
{
/// <summary>
/// The underlying file on disk.
/// </summary>
IFileInfo File { get; }
/// <summary>
/// The filename for this usage.
/// </summary>
string Filename { get; }
}
}
+1 -1
View File
@@ -115,7 +115,7 @@ namespace osu.Game.Database
createNewTask();
}
List<User> foundUsers = request.Result?.Users;
List<User> foundUsers = request.Response?.Users;
if (foundUsers != null)
{
+1 -1
View File
@@ -6,7 +6,7 @@ using osu.Game.Database;
namespace osu.Game.IO
{
public class FileInfo : IHasPrimaryKey
public class FileInfo : IHasPrimaryKey, IFileInfo
{
public int ID { get; set; }
+18
View File
@@ -0,0 +1,18 @@
// 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.
#nullable enable
namespace osu.Game.IO
{
/// <summary>
/// A representation of a tracked file.
/// </summary>
public interface IFileInfo
{
/// <summary>
/// SHA-256 hash of the file content.
/// </summary>
string Hash { get; }
}
}
+10 -5
View File
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
using osu.Framework.Logging;
@@ -17,7 +18,11 @@ namespace osu.Game.Online.API
{
protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest<T>(Uri);
public T Result { get; private set; }
/// <summary>
/// The deserialised response object. May be null if the request or deserialisation failed.
/// </summary>
[CanBeNull]
public T Response { get; private set; }
/// <summary>
/// Invoked on successful completion of an API request.
@@ -27,21 +32,21 @@ namespace osu.Game.Online.API
protected APIRequest()
{
base.Success += () => Success?.Invoke(Result);
base.Success += () => Success?.Invoke(Response);
}
protected override void PostProcess()
{
base.PostProcess();
Result = ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
Response = ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
}
internal void TriggerSuccess(T result)
{
if (Result != null)
if (Response != null)
throw new InvalidOperationException("Attempted to trigger success more than once");
Result = result;
Response = result;
TriggerSuccess();
}
+6 -2
View File
@@ -255,6 +255,7 @@ namespace osu.Game.Online.Leaderboards
}
private APIRequest getScoresRequest;
private ScheduledDelegate getScoresRequestCallback;
protected abstract bool IsOnlineScope { get; }
@@ -282,13 +283,16 @@ namespace osu.Game.Online.Leaderboards
getScoresRequest?.Cancel();
getScoresRequest = null;
getScoresRequestCallback?.Cancel();
getScoresRequestCallback = null;
pendingUpdateScores?.Cancel();
pendingUpdateScores = Schedule(() =>
{
PlaceholderState = PlaceholderState.Retrieving;
loading.Show();
getScoresRequest = FetchScores(scores => Schedule(() =>
getScoresRequest = FetchScores(scores => getScoresRequestCallback = Schedule(() =>
{
Scores = scores.ToArray();
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores;
@@ -297,7 +301,7 @@ namespace osu.Game.Online.Leaderboards
if (getScoresRequest == null)
return;
getScoresRequest.Failure += e => Schedule(() =>
getScoresRequest.Failure += e => getScoresRequestCallback = Schedule(() =>
{
if (e is OperationCanceledException)
return;
+1 -1
View File
@@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Music
{
Padding = new MarginPadding { Left = 5 };
FilterTerms = item.Metadata.SearchableTerms;
FilterTerms = item.Metadata.GetSearchableTerms();
}
[BackgroundDependencyLoader]
+11 -3
View File
@@ -143,19 +143,27 @@ namespace osu.Game.Overlays
switch (request)
{
case GetUserRankingsRequest userRequest:
if (userRequest.Response == null)
return null;
switch (userRequest.Type)
{
case UserRankingsType.Performance:
return new PerformanceTable(1, userRequest.Result.Users);
return new PerformanceTable(1, userRequest.Response.Users);
case UserRankingsType.Score:
return new ScoresTable(1, userRequest.Result.Users);
return new ScoresTable(1, userRequest.Response.Users);
}
return null;
case GetCountryRankingsRequest countryRequest:
return new CountriesTable(1, countryRequest.Result.Countries);
{
if (countryRequest.Response == null)
return null;
return new CountriesTable(1, countryRequest.Response.Countries);
}
}
return null;
@@ -110,10 +110,11 @@ namespace osu.Game.Rulesets.Difficulty
private void preProcess(Mod[] mods)
{
playableMods = mods.Select(m => m.DeepClone()).ToArray();
Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, playableMods);
var track = new TrackVirtual(10000);
mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
playableMods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
clockRate = track.Rate;
}
+1 -1
View File
@@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Edit
}
/// <summary>
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,BeatmapDifficulty, CancellationToken)"/>,
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,IBeatmapDifficultyInfo,CancellationToken)"/>,
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
/// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty);
+47
View File
@@ -0,0 +1,47 @@
// 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.
using System;
using osu.Game.Database;
#nullable enable
namespace osu.Game.Rulesets
{
/// <summary>
/// A representation of a ruleset's metadata.
/// </summary>
public interface IRulesetInfo : IHasOnlineID
{
/// <summary>
/// The user-exposed name of this ruleset.
/// </summary>
string Name { get; }
/// <summary>
/// An acronym defined by the ruleset that can be used as a permanent identifier.
/// </summary>
string ShortName { get; }
/// <summary>
/// A string representation of this ruleset, to be used with reflection to instantiate the ruleset represented by this metadata.
/// </summary>
string InstantiationInfo { get; }
Ruleset? CreateInstance()
{
var type = Type.GetType(InstantiationInfo);
if (type == null)
return null;
var ruleset = Activator.CreateInstance(type) as Ruleset;
// overwrite the pre-populated RulesetInfo with a potentially database attached copy.
// TODO: figure if we still want/need this after switching to realm.
// ruleset.RulesetInfo = this;
return ruleset;
}
}
}
@@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Mods
}
}
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty)
{
}
+1 -1
View File
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods
public override string Description => "Everything just got a bit harder...";
public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) };
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty)
{
}
+2 -2
View File
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Objects
/// <param name="controlPointInfo">The control points.</param>
/// <param name="difficulty">The difficulty settings to use.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty, CancellationToken cancellationToken = default)
public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default)
{
ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Objects
DefaultsApplied?.Invoke(this);
}
protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
Kiai = controlPointInfo.EffectPointAt(StartTime + control_point_leniency).KiaiMode;
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
public double Velocity = 1;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+7 -1
View File
@@ -10,7 +10,7 @@ using osu.Framework.Testing;
namespace osu.Game.Rulesets
{
[ExcludeFromDynamicCompile]
public class RulesetInfo : IEquatable<RulesetInfo>
public class RulesetInfo : IEquatable<RulesetInfo>, IRulesetInfo
{
public int? ID { get; set; }
@@ -54,5 +54,11 @@ namespace osu.Game.Rulesets
}
public override string ToString() => Name ?? $"{Name} ({ShortName}) ID: {ID}";
#region Implementation of IHasOnlineID
public int? OnlineID => ID;
#endregion
}
}
@@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Scoring
.First()
)));
targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target);
targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target);
// Add back a portion of the amount of HP to be drained, depending on the lenience requested.
targetMinimumHealth += drainLenience * (1 - targetMinimumHealth);
+1 -1
View File
@@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Scoring
{
foreach (var range in GetRanges())
{
var value = BeatmapDifficulty.DifficultyRange(difficulty, (range.Min, range.Average, range.Max));
var value = IBeatmapDifficultyInfo.DifficultyRange(difficulty, (range.Min, range.Average, range.Max));
switch (range.Result)
{
@@ -89,6 +89,13 @@ namespace osu.Game.Screens.Edit.Setup
{
public Action OnFocused;
protected override bool OnDragStart(DragStartEvent e)
{
// This text box is intended to be "read only" without actually specifying that.
// As such we don't want to allow the user to select its content with a drag.
return false;
}
protected override void OnFocus(FocusEvent e)
{
OnFocused?.Invoke();
@@ -66,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
req.Failure += exception =>
{
onError?.Invoke(req.Result?.Error ?? exception.Message);
onError?.Invoke(req.Response?.Error ?? exception.Message);
};
api.Queue(req);
@@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay
difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) };
beatmapText.Clear();
beatmapText.AddLink(Item.Beatmap.Value.ToRomanisableString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text =>
beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text =>
{
text.Truncate = true;
text.RelativeSizeAxes = Axes.X;
@@ -379,7 +379,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
if (item.NewValue?.Beatmap.Value != null)
{
statusText.Text = "Currently playing ";
beatmapText.AddLink(item.NewValue.Beatmap.Value.ToRomanisableString(),
beatmapText.AddLink(item.NewValue.Beatmap.Value.GetDisplayTitleRomanisable(),
LinkAction.OpenBeatmap,
item.NewValue.Beatmap.Value.OnlineBeatmapID.ToString(),
creationParameters: s =>
@@ -60,6 +60,8 @@ namespace osu.Game.Screens.Play.HUD
Current.Value = DisplayedCount = 0;
}
private Mod[] clonedMods;
[BackgroundDependencyLoader]
private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache)
{
@@ -67,8 +69,10 @@ namespace osu.Game.Screens.Play.HUD
if (gameplayState != null)
{
clonedMods = gameplayState.Mods.Select(m => m.DeepClone()).ToArray();
var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap);
difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token)
difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, clonedMods, loadCancellationSource.Token)
.ContinueWith(r => Schedule(() =>
{
timedAttributes = r.Result;
@@ -116,7 +120,11 @@ namespace osu.Game.Screens.Play.HUD
return;
}
var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, gameplayState.Score.ScoreInfo);
// awkward but we need to make sure the true mods are not passed to PerformanceCalculator as it makes a mess of track applications.
var scoreInfo = gameplayState.Score.ScoreInfo.DeepClone();
scoreInfo.Mods = clonedMods;
var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, scoreInfo);
Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero);
IsValid = true;
@@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select.Carousel
if (match)
{
var terms = BeatmapInfo.SearchableTerms;
var terms = BeatmapInfo.GetSearchableTerms();
foreach (var criteriaTerm in criteria.SearchTerms)
match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase));
@@ -106,12 +106,12 @@ namespace osu.Game.Screens.Select.Details
private void updateStatistics()
{
BeatmapDifficulty baseDifficulty = BeatmapInfo?.BaseDifficulty;
IBeatmapDifficultyInfo baseDifficulty = BeatmapInfo?.BaseDifficulty;
BeatmapDifficulty adjustedDifficulty = null;
if (baseDifficulty != null && mods.Value.Any(m => m is IApplicableToDifficulty))
{
adjustedDifficulty = baseDifficulty.Clone();
adjustedDifficulty = new BeatmapDifficulty(baseDifficulty);
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(adjustedDifficulty);
+11 -2
View File
@@ -83,8 +83,17 @@ namespace osu.Game.Tests.Visual
protected void PushAndConfirm(Func<Screen> newScreen)
{
Screen screen = null;
AddStep("Push new screen", () => Game.ScreenStack.Push(screen = newScreen()));
AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen == screen && screen.IsLoaded);
IScreen previousScreen = null;
AddStep("Push new screen", () =>
{
previousScreen = Game.ScreenStack.CurrentScreen;
Game.ScreenStack.Push(screen = newScreen());
});
AddUntilStep("Wait for new screen", () => screen.IsLoaded
&& Game.ScreenStack.CurrentScreen != previousScreen
&& previousScreen.GetChildScreen() == screen);
}
protected void ConfirmAtMainMenu() => AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded);
+4
View File
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Screens;
@@ -32,6 +33,9 @@ namespace osu.Game.Tests.Visual
content = new Container { RelativeSizeAxes = Axes.Both },
DialogOverlay = new DialogOverlay()
});
Stack.ScreenPushed += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}");
Stack.ScreenExited += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}");
}
protected void LoadScreen(OsuScreen screen) => Stack.Push(screen);