diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index bd6b857fe8..efd4b46782 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -25,6 +25,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 0b56405299..b826c1f546 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -25,12 +25,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty public override int Version => 20220701; - private readonly IWorkingBeatmap workingBeatmap; - public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { - workingBeatmap = beatmap; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -49,15 +46,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), }; - if (ComputeLegacyScoringValues) - { - CatchLegacyScoreSimulator sv1Simulator = new CatchLegacyScoreSimulator(); - sv1Simulator.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; - attributes.LegacyComboScore = sv1Simulator.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; - } - return attributes; } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs index c79fd36d96..0183d723b2 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs @@ -2,33 +2,26 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; namespace osu.Game.Rulesets.Catch.Difficulty { internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator { - public int AccuracyScore { get; private set; } - - public int ComboScore { get; private set; } - - public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; - private int legacyBonusScore; - private int modernBonusScore; + private int standardisedBonusScore; private int combo; private double scoreMultiplier; - public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) { IBeatmap baseBeatmap = workingBeatmap.Beatmap; @@ -70,13 +63,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty + baseBeatmap.Difficulty.CircleSize + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); - scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + scoreMultiplier = difficultyPeppyStars; + + LegacyScoreAttributes attributes = new LegacyScoreAttributes(); foreach (var obj in playableBeatmap.HitObjects) - simulateHit(obj); + simulateHit(obj, ref attributes); + + attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore; + + return attributes; } - private void simulateHit(HitObject hitObject) + private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes) { bool increaseCombo = true; bool addScoreComboMultiplier = false; @@ -112,28 +111,28 @@ namespace osu.Game.Rulesets.Catch.Difficulty case JuiceStream: foreach (var nested in hitObject.NestedHitObjects) - simulateHit(nested); + simulateHit(nested, ref attributes); return; case BananaShower: foreach (var nested in hitObject.NestedHitObjects) - simulateHit(nested); + simulateHit(nested, ref attributes); return; } if (addScoreComboMultiplier) { // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) - ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); + attributes.ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); } if (isBonus) { legacyBonusScore += scoreIncrease; - modernBonusScore += Judgement.ToNumericResult(bonusResult); + standardisedBonusScore += Judgement.ToNumericResult(bonusResult); } else - AccuracyScore += scoreIncrease; + attributes.AccuracyScore += scoreIncrease; if (increaseCombo) combo++; diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs index 26832b7271..c6f32e2014 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs @@ -2,17 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public partial class LegacyBananaPiece : LegacyCatchHitObjectPiece { + private static readonly Vector2 banana_max_size = new Vector2(128); + protected override void LoadComplete() { base.LoadComplete(); - Texture? texture = Skin.GetTexture("fruit-bananas"); - Texture? overlayTexture = Skin.GetTexture("fruit-bananas-overlay"); + Texture? texture = Skin.GetTexture("fruit-bananas")?.WithMaximumSize(banana_max_size); + Texture? overlayTexture = Skin.GetTexture("fruit-bananas-overlay")?.WithMaximumSize(banana_max_size); SetTexture(texture, overlayTexture); } diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs index 7ffd682698..c6c0839fba 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs @@ -2,12 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public partial class LegacyDropletPiece : LegacyCatchHitObjectPiece { + private static readonly Vector2 droplet_max_size = new Vector2(82, 103); + public LegacyDropletPiece() { Scale = new Vector2(0.8f); @@ -17,8 +20,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy { base.LoadComplete(); - Texture? texture = Skin.GetTexture("fruit-drop"); - Texture? overlayTexture = Skin.GetTexture("fruit-drop-overlay"); + Texture? texture = Skin.GetTexture("fruit-drop")?.WithMaximumSize(droplet_max_size); + Texture? overlayTexture = Skin.GetTexture("fruit-drop-overlay")?.WithMaximumSize(droplet_max_size); SetTexture(texture, overlayTexture); } diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs index 85b60561dd..62097d79bd 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs @@ -2,11 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { internal partial class LegacyFruitPiece : LegacyCatchHitObjectPiece { + private static readonly Vector2 fruit_max_size = new Vector2(128); + protected override void LoadComplete() { base.LoadComplete(); @@ -22,21 +26,26 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy switch (visualRepresentation) { case FruitVisualRepresentation.Pear: - SetTexture(Skin.GetTexture("fruit-pear"), Skin.GetTexture("fruit-pear-overlay")); + setTextures("pear"); break; case FruitVisualRepresentation.Grape: - SetTexture(Skin.GetTexture("fruit-grapes"), Skin.GetTexture("fruit-grapes-overlay")); + setTextures("grapes"); break; case FruitVisualRepresentation.Pineapple: - SetTexture(Skin.GetTexture("fruit-apple"), Skin.GetTexture("fruit-apple-overlay")); + setTextures("apple"); break; case FruitVisualRepresentation.Raspberry: - SetTexture(Skin.GetTexture("fruit-orange"), Skin.GetTexture("fruit-orange-overlay")); + setTextures("orange"); break; } + + void setTextures(string fruitName) => SetTexture( + Skin.GetTexture($"fruit-{fruitName}")?.WithMaximumSize(fruit_max_size), + Skin.GetTexture($"fruit-{fruitName}-overlay")?.WithMaximumSize(fruit_max_size) + ); } } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index de9f0d91ae..6bb6879052 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -31,13 +31,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty public override int Version => 20220902; - private readonly IWorkingBeatmap workingBeatmap; - public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { - workingBeatmap = beatmap; - isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } @@ -60,15 +56,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), }; - if (ComputeLegacyScoringValues) - { - ManiaLegacyScoreSimulator sv1Simulator = new ManiaLegacyScoreSimulator(); - sv1Simulator.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; - attributes.LegacyComboScore = sv1Simulator.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; - } - return attributes; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs index e544428979..098ccdf21d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs @@ -1,28 +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.Collections.Generic; -using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mania.Mods; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; namespace osu.Game.Rulesets.Mania.Difficulty { internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator { - public int AccuracyScore => 0; - public int ComboScore { get; private set; } - public double BonusScoreRatio => 0; - - public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) { - double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn)) - .Select(m => m.ScoreMultiplier) - .Aggregate(1.0, (c, n) => c * n); - - ComboScore = (int)(1000000 * multiplier); + return new LegacyScoreAttributes { ComboScore = 1000000 }; } } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 4507015169..b977c07dfb 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -33,6 +33,7 @@ using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs index cd85901a65..ef75e9df11 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -25,33 +26,42 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default { RelativeSizeAxes = Axes.Both; + // Avoid flickering due to no anti-aliasing of boxes by default. + var edgeSmoothness = new Vector2(0.3f); + AddInternal(mainLine = new Box { Name = "Bar line", + EdgeSmoothness = edgeSmoothness, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.Both, }); - Vector2 size = new Vector2(22, 6); - const float line_offset = 4; + const float major_extension = 10; - AddInternal(leftAnchor = new Circle + AddInternal(leftAnchor = new Box { Name = "Left anchor", + EdgeSmoothness = edgeSmoothness, + Blending = BlendingParameters.Additive, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreRight, - Size = size, - X = -line_offset, + Width = major_extension, + RelativeSizeAxes = Axes.Y, + Colour = ColourInfo.GradientHorizontal(Colour4.Transparent, Colour4.White), }); - AddInternal(rightAnchor = new Circle + AddInternal(rightAnchor = new Box { Name = "Right anchor", + EdgeSmoothness = edgeSmoothness, + Blending = BlendingParameters.Additive, Anchor = Anchor.CentreRight, Origin = Anchor.CentreLeft, - Size = size, - X = line_offset, + Width = major_extension, + RelativeSizeAxes = Axes.Y, + Colour = ColourInfo.GradientHorizontal(Colour4.White, Colour4.Transparent), }); major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy(); @@ -66,7 +76,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default private void updateMajor(ValueChangedEvent major) { mainLine.Alpha = major.NewValue ? 0.5f : 0.2f; - leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? 1 : 0; + leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0; } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 8cfd674f88..b1d9c453d6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -43,7 +43,8 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8); AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8); - PausableSkinnableSound getSpinningSample() => drawableSpinner.ChildrenOfType().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin")))); + PausableSkinnableSound getSpinningSample() => + drawableSpinner.ChildrenOfType().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin")))); } [TestCase(false)] @@ -64,6 +65,39 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1); } + [TestCase(0, 4, 6)] + [TestCase(5, 7, 10)] + [TestCase(10, 11, 8)] + public void TestSpinnerSpinRequirements(int od, int normalTicks, int bonusTicks) + { + Spinner spinner = null; + + AddStep("add spinner", () => SetContents(_ => + { + spinner = new Spinner + { + StartTime = Time.Current, + EndTime = Time.Current + 3000, + Samples = new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + } + }; + + spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { OverallDifficulty = od }); + + return drawableSpinner = new TestDrawableSpinner(spinner, true) + { + Anchor = Anchor.Centre, + Depth = depthIndex++, + Scale = new Vector2(0.75f) + }; + })); + + AddAssert("number of normal ticks matches", () => spinner.SpinsRequired, () => Is.EqualTo(normalTicks)); + AddAssert("number of bonus ticks matches", () => spinner.MaximumBonusSpins, () => Is.EqualTo(bonusTicks)); + } + private Drawable testSingle(float circleSize, bool auto = false, double length = 3000) { const double delay = 2000; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index b92092c674..3b580a5b59 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -26,12 +26,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty public override int Version => 20220902; - private readonly IWorkingBeatmap workingBeatmap; - public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { - workingBeatmap = beatmap; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -109,15 +106,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpinnerCount = spinnerCount, }; - if (ComputeLegacyScoringValues) - { - OsuLegacyScoreSimulator sv1Simulator = new OsuLegacyScoreSimulator(); - sv1Simulator.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; - attributes.LegacyComboScore = sv1Simulator.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; - } - return attributes; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs index 980d86e4ad..952ffa5e7a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs @@ -2,37 +2,27 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; namespace osu.Game.Rulesets.Osu.Difficulty { internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator { - public int AccuracyScore { get; private set; } - - public int ComboScore { get; private set; } - - public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; - private int legacyBonusScore; - private int modernBonusScore; + private int standardisedBonusScore; private int combo; private double scoreMultiplier; - private IBeatmap playableBeatmap = null!; - public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) { - this.playableBeatmap = playableBeatmap; - IBeatmap baseBeatmap = workingBeatmap.Beatmap; int countNormal = 0; @@ -73,13 +63,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty + baseBeatmap.Difficulty.CircleSize + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); - scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + scoreMultiplier = difficultyPeppyStars; + + LegacyScoreAttributes attributes = new LegacyScoreAttributes(); foreach (var obj in playableBeatmap.HitObjects) - simulateHit(obj); + simulateHit(obj, ref attributes); + + attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore; + + return attributes; } - private void simulateHit(HitObject hitObject) + private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes) { bool increaseCombo = true; bool addScoreComboMultiplier = false; @@ -122,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty case Slider: foreach (var nested in hitObject.NestedHitObjects) - simulateHit(nested); + simulateHit(nested, ref attributes); scoreIncrease = 300; increaseCombo = false; @@ -133,22 +129,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty // The spinner object applies a lenience because gameplay mechanics differ from osu-stable. // We'll redo the calculations to match osu-stable here... const double maximum_rotations_per_second = 477.0 / 60; - double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); + + // Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises bonus score. + // As we're primarily concerned with computing the maximum theoretical final score, + // this will have the final effect of slightly underestimating bonus score achieved on stable when converting from score V1. + const double minimum_rotations_per_second = 3; + double secondsDuration = spinner.Duration / 1000; // The total amount of half spins possible for the entire spinner. int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2); // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). - int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond); + int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimum_rotations_per_second); // To be able to receive bonus points, the spinner must be rotated another 1.5 times. int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3; for (int i = 0; i <= totalHalfSpinsPossible; i++) { if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0) - simulateHit(new SpinnerBonusTick()); + simulateHit(new SpinnerBonusTick(), ref attributes); else if (i > 1 && i % 2 == 0) - simulateHit(new SpinnerTick()); + simulateHit(new SpinnerTick(), ref attributes); } scoreIncrease = 300; @@ -159,16 +160,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (addScoreComboMultiplier) { // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) - ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); + attributes.ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); } if (isBonus) { legacyBonusScore += scoreIncrease; - modernBonusScore += Judgement.ToNumericResult(bonusResult); + standardisedBonusScore += Judgement.ToNumericResult(bonusResult); } else - AccuracyScore += scoreIncrease; + attributes.AccuracyScore += scoreIncrease; if (increaseCombo) combo++; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs index e5cc8595d1..3cba0610a1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; InternalChild = content = new Container { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs index 670e98ca50..c585f09b00 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; CornerRadius = Size.X / 2; CornerExponent = 2; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 6beed0294d..2f1cf57ebe 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public HitReceptor() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index d06fb5b4de..47214f1e53 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Children = new[] { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index fc4863f164..5721328057 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void load() { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; AddInternal(scaleContainer = new Container { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index d9501f7d58..9fbc97c484 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void load() { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; AddRangeInternal(new Drawable[] { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 6d0ae93e62..a947580d2f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [BackgroundDependencyLoader] private void load() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Origin = Anchor.Centre; AddInternal(scaleContainer = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index fd5741698a..0bdbfaa760 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -21,6 +21,11 @@ namespace osu.Game.Rulesets.Osu.Objects /// public const float OBJECT_RADIUS = 64; + /// + /// The width and height any element participating in display of a hitcircle (or similarly sized object) should be. + /// + public static readonly Vector2 OBJECT_DIMENSIONS = new Vector2(OBJECT_RADIUS * 2); + /// /// Scoring distance with a speed-adjusted beat length of 1 second (ie. the speed slider balls move through their track). /// diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 5ef670c739..aca55c6bd9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -18,6 +18,16 @@ namespace osu.Game.Rulesets.Osu.Objects { public class Spinner : OsuHitObject, IHasDuration { + /// + /// The RPM required to clear the spinner at ODs [ 0, 5, 10 ]. + /// + private static readonly (int min, int mid, int max) clear_rpm_range = (90, 150, 225); + + /// + /// The RPM required to complete the spinner and receive full score at ODs [ 0, 5, 10 ]. + /// + private static readonly (int min, int mid, int max) complete_rpm_range = (250, 380, 430); + public double EndTime { get => StartTime + Duration; @@ -52,13 +62,16 @@ namespace osu.Game.Rulesets.Osu.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - const double maximum_rotations_per_second = 477f / 60f; + // The average RPS required over the length of the spinner to clear the spinner. + double minRps = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, clear_rpm_range) / 60; + + // The RPS required over the length of the spinner to receive full score (all normal + bonus ticks). + double maxRps = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, complete_rpm_range) / 60; double secondsDuration = Duration / 1000; - double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 1.5, 2.5, 3.75); - SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond); - MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration) - bonus_spins_gap; + SpinsRequired = (int)(minRps * secondsDuration); + MaximumBonusSpins = Math.Max(0, (int)(maxRps * secondsDuration) - SpinsRequired - bonus_spins_gap); } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 220dc53705..607b83d379 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -33,6 +33,7 @@ using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs index 3427031dc8..7508a689d2 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon private Bindable configHitLighting = null!; - private static readonly Vector2 circle_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + private static readonly Vector2 circle_size = OsuHitObject.OBJECT_DIMENSIONS; [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs index f93e26b2ca..67fc1b2304 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon Anchor = Anchor.Centre; Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; InternalChildren = new Drawable[] { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs index f4761e0ea8..65a7b1328b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; -using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default { @@ -22,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public CirclePiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Masking = true; CornerRadius = Size.X / 2; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs index 91bf75617a..7beb16f7d7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; -using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default { @@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public ExplodePiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs index 789137117e..86087ac50d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; -using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default { @@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { public FlashPiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs index 20fa4e5342..bcea33f63c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Default @@ -25,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public MainCirclePiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs index 3fe7872ff7..27868db2f6 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Anchor = Anchor.Centre; Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Child = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow), _ => new SpriteIcon { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs index 46d48f62e7..c3bbd89ab6 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Default @@ -14,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { public RingPiece(float thickness = 9) { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs index e9342bbdbb..403a14214e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs @@ -5,12 +5,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { + // todo: this should probably not be a SkinnableSprite, as this is always created for legacy skins and is recreated on skin change. public partial class LegacyApproachCircle : SkinnableSprite { private readonly IBindable accentColour = new Bindable(); @@ -19,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private DrawableHitObject drawableObject { get; set; } = null!; public LegacyApproachCircle() - : base("Gameplay/osu/approachcircle") + : base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS) { } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index cadac4d319..8990204931 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy @@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy this.priorityLookupPrefix = priorityLookupPrefix; this.hasNumber = hasNumber; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; } [BackgroundDependencyLoader] @@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png. InternalChildren = new[] { - CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) }) + CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -77,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d)) + Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d, maxSize: OsuHitObject.OBJECT_DIMENSIONS)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index e6166e9441..3a80607522 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK.Graphics; @@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null); - InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true) ?? Empty()); + InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true, maxSize: OsuHitObject.OBJECT_DIMENSIONS) ?? Empty()); textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs index 2aa843581e..c3beb5bc35 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK.Graphics; @@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = skin.GetTexture("sliderb-nd"), + Texture = skin.GetTexture("sliderb-nd")?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS), Colour = new Color4(5, 5, 5, 255), }, LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d => @@ -58,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = skin.GetTexture("sliderb-spec"), + Texture = skin.GetTexture("sliderb-spec")?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS), Blending = BlendingParameters.Additive, }, }; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index f049aa088f..ea6f6fe6ce 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; using osuTK; @@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy /// Their hittable area is 128px, but the actual circle portion is 118px. /// We must account for some gameplay elements such as slider bodies, where this padding is not present. /// - public const float LEGACY_CIRCLE_RADIUS = 64 - 5; + public const float LEGACY_CIRCLE_RADIUS = OsuHitObject.OBJECT_RADIUS - 5; public OsuLegacySkinTransformer(ISkin skin) : base(skin) @@ -41,14 +42,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return this.GetAnimation("sliderscorepoint", false, false); case OsuSkinComponents.SliderFollowCircle: - var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true); + var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: new Vector2(308f)); if (followCircleContent != null) return new LegacyFollowCircle(followCircleContent); return null; case OsuSkinComponents.SliderBall: - var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: ""); + var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "", maxSize: OsuHitObject.OBJECT_DIMENSIONS); // todo: slider ball has a custom frame delay based on velocity // Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME); @@ -138,7 +139,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (!this.HasFont(LegacyFont.HitCircle)) return null; - return new LegacySpriteText(LegacyFont.HitCircle) + return new LegacySpriteText(LegacyFont.HitCircle, OsuHitObject.OBJECT_DIMENSIONS) { // stable applies a blanket 0.8x scale to hitcircle fonts Scale = new Vector2(0.8f), diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs index 076d97d06a..52486b701a 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { new RingPiece(3) { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2), + Size = OsuHitObject.OBJECT_DIMENSIONS, Alpha = 0.1f, } }; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 25adba5ab6..ab193caaa3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -25,12 +25,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public override int Version => 20220902; - private readonly IWorkingBeatmap workingBeatmap; - public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { - workingBeatmap = beatmap; } protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) @@ -99,15 +96,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty MaxCombo = beatmap.HitObjects.Count(h => h is Hit), }; - if (ComputeLegacyScoringValues) - { - TaikoLegacyScoreSimulator sv1Simulator = new TaikoLegacyScoreSimulator(); - sv1Simulator.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; - attributes.LegacyComboScore = sv1Simulator.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; - } - return attributes; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs index e77327d622..d4538d3c00 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs @@ -2,39 +2,29 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty { internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator { - public int AccuracyScore { get; private set; } - - public int ComboScore { get; private set; } - - public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; - private int legacyBonusScore; - private int modernBonusScore; + private int standardisedBonusScore; private int combo; - private double modMultiplier; private int difficultyPeppyStars; private IBeatmap playableBeatmap = null!; - private IReadOnlyList mods = null!; - public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) { this.playableBeatmap = playableBeatmap; - this.mods = mods; IBeatmap baseBeatmap = workingBeatmap.Beatmap; @@ -76,13 +66,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty + baseBeatmap.Difficulty.CircleSize + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); - modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + LegacyScoreAttributes attributes = new LegacyScoreAttributes(); foreach (var obj in playableBeatmap.HitObjects) - simulateHit(obj); + simulateHit(obj, ref attributes); + + attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore; + + return attributes; } - private void simulateHit(HitObject hitObject) + private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes) { bool increaseCombo = true; bool addScoreComboMultiplier = false; @@ -109,21 +103,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty case Swell swell: // The taiko swell generally does not match the osu-stable implementation in any way. // We'll redo the calculations to match osu-stable here... - double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); - double secondsDuration = swell.Duration / 1000; + + // Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises rotations. + const double minimum_rotations_per_second = 7.5; // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). - int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond); - + int halfSpinsRequiredForCompletion = (int)(swell.Duration / 1000 * minimum_rotations_per_second); halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f); - if (mods.Any(m => m is ModDoubleTime)) - halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 0.75f)); - if (mods.Any(m => m is ModHalfTime)) - halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f)); + // + // Normally, this multiplier depends on the active mods (DT = 0.75, HT = 1.5). For simplicity, we'll only consider the worst case that maximises rotations. + // This way, scores remain beatable at the cost of the conversion being slightly inaccurate. + // - A perfect DT/NM score will have less than 1M total score (excluding bonus). + // - A perfect HT score will have 1M total score (excluding bonus). + // + halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f)); for (int i = 0; i <= halfSpinsRequiredForCompletion; i++) - simulateHit(new SwellTick()); + simulateHit(new SwellTick(), ref attributes); scoreIncrease = 300; addScoreComboMultiplier = true; @@ -139,7 +136,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty case DrumRoll: foreach (var nested in hitObject.NestedHitObjects) - simulateHit(nested); + simulateHit(nested, ref attributes); return; } @@ -159,8 +156,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { int oldScoreIncrease = scoreIncrease; - // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) - scoreIncrease += (int)(scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * modMultiplier) * (Math.Min(100, combo) / 10); + scoreIncrease += scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * (Math.Min(100, combo) / 10); if (hitObject is Swell) { @@ -185,15 +181,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty scoreIncrease -= comboScoreIncrease; if (addScoreComboMultiplier) - ComboScore += comboScoreIncrease; + attributes.ComboScore += comboScoreIncrease; if (isBonus) { legacyBonusScore += scoreIncrease; - modernBonusScore += Judgement.ToNumericResult(bonusResult); + standardisedBonusScore += Judgement.ToNumericResult(bonusResult); } else - AccuracyScore += scoreIncrease; + attributes.AccuracyScore += scoreIncrease; if (increaseCombo) combo++; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs index 5516e025cd..c94016d2b1 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public partial class LegacyCirclePiece : CompositeDrawable, IHasAccentColour { + private static readonly Vector2 circle_piece_size = new Vector2(128); + private Drawable backgroundLayer = null!; private Drawable? foregroundLayer; @@ -52,9 +54,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit; - return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? + return skin.GetAnimation($"{prefix}{lookup}", true, false, maxSize: circle_piece_size) ?? // fallback to regular size if "big" version doesn't exist. - skin.GetAnimation($"{normal_hit}{lookup}", true, false); + skin.GetAnimation($"{normal_hit}{lookup}", true, false, maxSize: circle_piece_size); } // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer. @@ -96,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy // Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay". // This ensures they are scaled relative to each other but also match the expected DrawableHit size. foreach (var c in InternalChildren) - c.Scale = new Vector2(DrawHeight / 128); + c.Scale = new Vector2(DrawHeight / circle_piece_size.Y); if (foregroundLayer is IFramedAnimation animatableForegroundLayer) animateForegroundLayer(animatableForegroundLayer); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 584a91bfdc..072653dcbf 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -34,6 +34,7 @@ using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; using osu.Game.Rulesets.Configuration; using osu.Game.Configuration; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.Taiko.Configuration; namespace osu.Game.Rulesets.Taiko diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 920e560018..e6c3eea925 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -125,7 +125,7 @@ namespace osu.Game.Tests.Visual.Editing }); AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual); - AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); + AddUntilStep("track length changed", () => Beatmap.Value.Track.Length > 60000); AddStep("test play", () => Editor.TestGameplay()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs new file mode 100644 index 0000000000..a8ed44c7f8 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.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 System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Textures; +using osu.Framework.Testing; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Screens.Play; +using osu.Game.Skinning; + +namespace osu.Game.Tests.Visual.Gameplay +{ + /// + /// Upscales all gameplay sprites by a huge amount, to aid in manually checking skin texture size limits + /// on individual elements. + /// + /// + /// The HUD is hidden as it does't really affect game balance if HUD elements are larger than they should be. + /// + public partial class TestScenePlayerMaxDimensions : TestSceneAllRulesetPlayers + { + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + // for now this only applies to legacy skins, as modern skins don't have texture-based gameplay elements yet. + dependencies.CacheAs(new UpscaledLegacySkin(dependencies.Get())); + + return dependencies; + } + + protected override void AddCheckSteps() + { + } + + protected override Player CreatePlayer(Ruleset ruleset) + { + var player = base.CreatePlayer(ruleset); + player.OnLoadComplete += _ => + { + // this test scene focuses on gameplay elements, so let's hide the hud. + var hudOverlay = player.ChildrenOfType().Single(); + hudOverlay.ShowHud.Value = false; + hudOverlay.ShowHud.Disabled = true; + }; + return player; + } + + private class UpscaledLegacySkin : DefaultLegacySkin, ISkinSource + { + public UpscaledLegacySkin(IStorageResourceProvider resources) + : base(resources) + { + } + + public event Action? SourceChanged + { + add { } + remove { } + } + + public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) + { + var texture = base.GetTexture(componentName, wrapModeS, wrapModeT); + + if (texture != null) + texture.ScaleAdjust /= 8f; + + return texture; + } + + public ISkin FindProvider(Func lookupFunction) => this; + public IEnumerable AllSources => new[] { this }; + } + } +} diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 11a5e252a3..dd7d7c7b1b 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Scoring; namespace osu.Game.Database @@ -222,15 +223,9 @@ namespace osu.Game.Database throw new InvalidOperationException("Beatmap contains no hit objects!"); ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); + LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap); - sv1Simulator.Simulate(beatmap, playableBeatmap, mods); - - return ConvertFromLegacyTotalScore(score, new DifficultyAttributes - { - LegacyAccuracyScore = sv1Simulator.AccuracyScore, - LegacyComboScore = sv1Simulator.ComboScore, - LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio - }); + return ConvertFromLegacyTotalScore(score, attributes); } /// @@ -241,20 +236,21 @@ namespace osu.Game.Database /// (, , and ) /// for the beatmap which the score was set on. /// The standardised total score. - public static long ConvertFromLegacyTotalScore(ScoreInfo score, DifficultyAttributes attributes) + public static long ConvertFromLegacyTotalScore(ScoreInfo score, LegacyScoreAttributes attributes) { if (!score.IsLegacyScore) return score.TotalScore; Debug.Assert(score.LegacyTotalScore != null); - int maximumLegacyAccuracyScore = attributes.LegacyAccuracyScore; - int maximumLegacyComboScore = attributes.LegacyComboScore; - double maximumLegacyBonusRatio = attributes.LegacyBonusScoreRatio; double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); + int maximumLegacyAccuracyScore = attributes.AccuracyScore; + long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * modMultiplier); + double maximumLegacyBonusRatio = attributes.BonusScoreRatio; + // The part of total score that doesn't include bonus. - int maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; + long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; // The combo proportion is calculated as a proportion of maximumLegacyBaseScore. double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore); diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index f62eeab5d7..6043731e6e 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -27,9 +27,6 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; - protected const int ATTRIB_ID_LEGACY_ACCURACY_SCORE = 23; - protected const int ATTRIB_ID_LEGACY_COMBO_SCORE = 25; - protected const int ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO = 27; /// /// The mods which were applied to the beatmap. @@ -91,9 +88,6 @@ namespace osu.Game.Rulesets.Difficulty public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); - yield return (ATTRIB_ID_LEGACY_ACCURACY_SCORE, LegacyAccuracyScore); - yield return (ATTRIB_ID_LEGACY_COMBO_SCORE, LegacyComboScore); - yield return (ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO, LegacyBonusScoreRatio); } /// @@ -104,11 +98,6 @@ namespace osu.Game.Rulesets.Difficulty public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; - - // Temporarily allow these attributes to not exist so as to not block releases of server-side components while these attributes aren't populated/used yet. - LegacyAccuracyScore = (int)values.GetValueOrDefault(ATTRIB_ID_LEGACY_ACCURACY_SCORE); - LegacyComboScore = (int)values.GetValueOrDefault(ATTRIB_ID_LEGACY_COMBO_SCORE); - LegacyBonusScoreRatio = values.GetValueOrDefault(ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO); } } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index d005bbfc7a..00c90bd317 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -23,13 +23,6 @@ namespace osu.Game.Rulesets.Difficulty { public abstract class DifficultyCalculator { - /// - /// Whether legacy scoring values (ScoreV1) should be computed to populate the difficulty attributes - /// , , - /// and . - /// - public bool ComputeLegacyScoringValues; - /// /// The beatmap for which difficulty will be calculated. /// diff --git a/osu.Game/Rulesets/ILegacyRuleset.cs b/osu.Game/Rulesets/ILegacyRuleset.cs index 24aa672219..6900afa243 100644 --- a/osu.Game/Rulesets/ILegacyRuleset.cs +++ b/osu.Game/Rulesets/ILegacyRuleset.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 osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; namespace osu.Game.Rulesets { diff --git a/osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs b/osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs deleted file mode 100644 index 7240f0d73e..0000000000 --- a/osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Rulesets.Scoring -{ - /// - /// Generates attributes which are required to calculate old-style Score V1 scores. - /// - public interface ILegacyScoreSimulator - { - /// - /// The accuracy portion of the legacy (ScoreV1) total score. - /// - int AccuracyScore { get; } - - /// - /// The combo-multiplied portion of the legacy (ScoreV1) total score. - /// - int ComboScore { get; } - - /// - /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. - /// This is made up of all judgements that would be or . - /// - double BonusScoreRatio { get; } - - /// - /// Performs the simulation, computing the maximum , , - /// and achievable for the given beatmap. - /// - /// The working beatmap. - /// A playable version of the beatmap for the ruleset. - /// The applied mods. - void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods); - } -} diff --git a/osu.Game/Rulesets/Scoring/Legacy/ILegacyScoreSimulator.cs b/osu.Game/Rulesets/Scoring/Legacy/ILegacyScoreSimulator.cs new file mode 100644 index 0000000000..fe7843a682 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/Legacy/ILegacyScoreSimulator.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; + +namespace osu.Game.Rulesets.Scoring.Legacy +{ + /// + /// Generates attributes which are required to calculate old-style Score V1 scores. + /// + public interface ILegacyScoreSimulator + { + /// + /// Performs the simulation, computing the maximum scoring values achievable for the given beatmap. + /// + /// The working beatmap. + /// A playable version of the beatmap for the ruleset. + LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap); + } +} diff --git a/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreAttributes.cs b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreAttributes.cs new file mode 100644 index 0000000000..47ab68bf88 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreAttributes.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. + +namespace osu.Game.Rulesets.Scoring.Legacy +{ + public struct LegacyScoreAttributes + { + /// + /// The accuracy portion of the legacy (ScoreV1) total score. + /// + public int AccuracyScore; + + /// + /// The combo-multiplied portion of the legacy (ScoreV1) total score. + /// + public long ComboScore; + + /// + /// A ratio of standardised score to legacy score for the bonus part of total score. + /// + public double BonusScoreRatio; + } +} diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs index b6a944ddf8..91f1171a72 100644 --- a/osu.Game/Skinning/IAnimationTimeReference.cs +++ b/osu.Game/Skinning/IAnimationTimeReference.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Textures; using osu.Framework.Timing; +using osuTK; namespace osu.Game.Skinning { @@ -13,7 +14,7 @@ namespace osu.Game.Skinning /// /// /// This should not be used to start an animation immediately at the current time. - /// To do so, use with startAtCurrentTime = true instead. + /// To do so, use with startAtCurrentTime = true instead. /// [Cached] public interface IAnimationTimeReference diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 0d2461567f..dde6c1fa29 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osuTK; using static osu.Game.Skinning.SkinConfiguration; namespace osu.Game.Skinning @@ -18,16 +19,16 @@ namespace osu.Game.Skinning public static partial class LegacySkinExtensions { public static Drawable? GetAnimation(this ISkin? source, string componentName, bool animatable, bool looping, bool applyConfigFrameRate = false, string animationSeparator = "-", - bool startAtCurrentTime = true, double? frameLength = null) - => source.GetAnimation(componentName, default, default, animatable, looping, applyConfigFrameRate, animationSeparator, startAtCurrentTime, frameLength); + bool startAtCurrentTime = true, double? frameLength = null, Vector2? maxSize = null) + => source.GetAnimation(componentName, default, default, animatable, looping, applyConfigFrameRate, animationSeparator, startAtCurrentTime, frameLength, maxSize); public static Drawable? GetAnimation(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, bool looping, bool applyConfigFrameRate = false, - string animationSeparator = "-", bool startAtCurrentTime = true, double? frameLength = null) + string animationSeparator = "-", bool startAtCurrentTime = true, double? frameLength = null, Vector2? maxSize = null) { if (source == null) return null; - var textures = GetTextures(source, componentName, wrapModeS, wrapModeT, animatable, animationSeparator, out var retrievalSource); + var textures = GetTextures(source, componentName, wrapModeS, wrapModeT, animatable, animationSeparator, maxSize, out var retrievalSource); switch (textures.Length) { @@ -53,7 +54,7 @@ namespace osu.Game.Skinning } } - public static Texture[] GetTextures(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, string animationSeparator, out ISkin? retrievalSource) + public static Texture[] GetTextures(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, string animationSeparator, Vector2? maxSize, out ISkin? retrievalSource) { retrievalSource = null; @@ -80,6 +81,9 @@ namespace osu.Game.Skinning // if an animation was not allowed or not found, fall back to a sprite retrieval. var singleTexture = retrievalSource.GetTexture(componentName, wrapModeS, wrapModeT); + if (singleTexture != null && maxSize != null) + singleTexture = singleTexture.WithMaximumSize(maxSize.Value); + return singleTexture != null ? new[] { singleTexture } : Array.Empty(); @@ -88,11 +92,14 @@ namespace osu.Game.Skinning { for (int i = 0; true; i++) { - Texture? texture; + var texture = skin.GetTexture(getFrameName(i), wrapModeS, wrapModeT); - if ((texture = skin.GetTexture(getFrameName(i), wrapModeS, wrapModeT)) == null) + if (texture == null) break; + if (maxSize != null) + texture = texture.WithMaximumSize(maxSize.Value); + yield return texture; } } @@ -100,6 +107,16 @@ namespace osu.Game.Skinning string getFrameName(int frameIndex) => $"{componentName}{animationSeparator}{frameIndex}"; } + public static Texture WithMaximumSize(this Texture texture, Vector2 maxSize) + { + if (texture.DisplayWidth <= maxSize.X && texture.DisplayHeight <= maxSize.Y) + return texture; + + // use scale adjust property for downscaling the texture in order to meet the specified maximum dimensions. + texture.ScaleAdjust *= Math.Max(texture.DisplayWidth / maxSize.X, texture.DisplayHeight / maxSize.Y); + return texture; + } + public static bool HasFont(this ISkin source, LegacyFont font) { return source.GetTexture($"{source.GetFontPrefix(font)}-0") != null; diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index d6af52855b..7eb92126fa 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -13,6 +13,7 @@ namespace osu.Game.Skinning public sealed partial class LegacySpriteText : OsuSpriteText { private readonly LegacyFont font; + private readonly Vector2? maxSizePerGlyph; private LegacyGlyphStore glyphStore = null!; @@ -20,9 +21,11 @@ namespace osu.Game.Skinning protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; - public LegacySpriteText(LegacyFont font) + public LegacySpriteText(LegacyFont font, Vector2? maxSizePerGlyph = null) { this.font = font; + this.maxSizePerGlyph = maxSizePerGlyph; + Shadow = false; UseFullGlyphHeight = false; } @@ -33,7 +36,7 @@ namespace osu.Game.Skinning Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true); Spacing = new Vector2(-skin.GetFontOverlap(font), 0); - glyphStore = new LegacyGlyphStore(skin); + glyphStore = new LegacyGlyphStore(skin, maxSizePerGlyph); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); @@ -41,10 +44,12 @@ namespace osu.Game.Skinning private class LegacyGlyphStore : ITexturedGlyphLookupStore { private readonly ISkin skin; + private readonly Vector2? maxSize; - public LegacyGlyphStore(ISkin skin) + public LegacyGlyphStore(ISkin skin, Vector2? maxSize) { this.skin = skin; + this.maxSize = maxSize; } public ITexturedCharacterGlyph? Get(string fontName, char character) @@ -56,6 +61,9 @@ namespace osu.Game.Skinning if (texture == null) return null; + if (maxSize != null) + texture = texture.WithMaximumSize(maxSize.Value); + return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 1f / texture.ScaleAdjust); } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 659edc6f5f..5ae4852288 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -197,7 +197,7 @@ namespace osu.Game.Skinning { // This fallback is important for user skins which use SkinnableSprites. case SkinnableSprite.SpriteComponentLookup sprite: - return this.GetAnimation(sprite.LookupName, false, false); + return this.GetAnimation(sprite.LookupName, false, false, maxSize: sprite.MaxSize); case SkinComponentsContainerLookup containerLookup: diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 1d97566470..9effb483c4 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -34,8 +34,8 @@ namespace osu.Game.Skinning [Resolved] private ISkinSource source { get; set; } = null!; - public SkinnableSprite(string textureName, ConfineMode confineMode = ConfineMode.NoScaling) - : base(new SpriteComponentLookup(textureName), confineMode) + public SkinnableSprite(string textureName, Vector2? maxSize = null, ConfineMode confineMode = ConfineMode.NoScaling) + : base(new SpriteComponentLookup(textureName, maxSize), confineMode) { SpriteName.Value = textureName; } @@ -56,10 +56,14 @@ namespace osu.Game.Skinning protected override Drawable CreateDefault(ISkinComponentLookup lookup) { - var texture = textures.Get(((SpriteComponentLookup)lookup).LookupName); + var spriteLookup = (SpriteComponentLookup)lookup; + var texture = textures.Get(spriteLookup.LookupName); if (texture == null) - return new SpriteNotFound(((SpriteComponentLookup)lookup).LookupName); + return new SpriteNotFound(spriteLookup.LookupName); + + if (spriteLookup.MaxSize != null) + texture = texture.WithMaximumSize(spriteLookup.MaxSize.Value); return new Sprite { Texture = texture }; } @@ -69,10 +73,12 @@ namespace osu.Game.Skinning internal class SpriteComponentLookup : ISkinComponentLookup { public string LookupName { get; set; } + public Vector2? MaxSize { get; set; } - public SpriteComponentLookup(string textureName) + public SpriteComponentLookup(string textureName, Vector2? maxSize = null) { LookupName = textureName; + MaxSize = maxSize; } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 054a50456b..ea981d2e6d 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -135,7 +135,7 @@ namespace osu.Game.Storyboards.Drawables // When reading from a skin, we match stables weird behaviour where `FrameCount` is ignored // and resources are retrieved until the end of the animation. - foreach (var texture in skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path)!, default, default, true, string.Empty, out _)) + foreach (var texture in skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path)!, default, default, true, string.Empty, null, out _)) AddFrame(texture, Animation.FrameDelay); }