diff --git a/osu.Android.props b/osu.Android.props
index c88bea8265..f4d08e443c 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -11,7 +11,7 @@
manifestmerger.jar
-
+
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index 3341f834dd..6292ed75cd 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
- return 200000 * comboProgress
- + 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ return 10000 * comboProgress
+ + 990000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ bonusPortion;
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs
index 3b8a5a90a5..9af1855167 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("place first object", () => InputManager.Click(MouseButton.Left));
- AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0)));
+ AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.01f, 0)));
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2));
- AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.235f, 0)));
+ AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.205f, 0)));
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
@@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
- AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0)));
+ AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.01f, 0)));
AddAssert("object 3 snapped to 1", () =>
{
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return Precision.AlmostEquals(first.EndPosition, third.Position);
});
- AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.22f, playfield.ScreenSpaceDrawQuad.Width * 0.21f)));
+ AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.21f, playfield.ScreenSpaceDrawQuad.Width * 0.205f)));
AddAssert("object 2 snapped to 1", () =>
{
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
index 616a9c362d..32028823ae 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -1,22 +1,29 @@
// 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 NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModAutoplay : OsuModTestScene
{
+ protected override bool AllowFail => true;
+
[Test]
public void TestCursorPositionStoredToJudgement()
{
@@ -44,6 +51,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
FinalRate = { Value = 1.3 }
});
+ [Test]
+ public void TestPerfectScoreOnShortSliderWithRepeat()
+ {
+ AddStep("set score to standardised", () => LocalConfig.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
+
+ CreateModTest(new ModTestData
+ {
+ Autoplay = true,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 500,
+ Position = new Vector2(256, 192),
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(0, 6.25f))
+ }),
+ RepeatCount = 1,
+ SliderVelocity = 10
+ }
+ }
+ },
+ PassCondition = () => Player.ScoreProcessor.TotalScore.Value == 1_000_000
+ });
+ }
+
private void runSpmTest(Mod mod)
{
SpinnerSpmCalculator? spmCalculator = null;
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index ad6af6d74e..aac5f6ffb1 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Osu.Edit
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
float snapRadius =
- playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS / 5)).X -
+ playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS * 0.10f)).X -
playfield.GamefieldToScreenSpace(Vector2.Zero).X;
foreach (var b in blueprints)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 664a8146e7..01174d4d61 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -75,18 +75,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both };
+
AddRangeInternal(new Drawable[]
{
shakeContainer = new ShakeContainer
{
ShakeDuration = 30,
RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
+ Children = new[]
{
Body = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
- tailContainer = new Container { RelativeSizeAxes = Axes.Both },
+ // proxied here so that the tail is drawn under repeats/ticks - legacy skins rely on this
+ tailContainer.CreateProxy(),
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
+ // actual tail container is placed here to ensure that tail hitobjects are processed after ticks/repeats.
+ // this is required for the correct operation of Score V2.
+ tailContainer,
}
},
// slider head is not included in shake as it handles hit detection, and handles its own shaking.
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
index 1c5cf49625..3427031dc8 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
@@ -48,21 +48,26 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private Bindable configHitLighting = null!;
+ private static readonly Vector2 circle_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
+
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
public ArgonMainCirclePiece(bool withOuterFill)
{
- Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
+ Size = circle_size;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
- outerFill = new Circle // renders white outer border and dark fill
+ outerFill = new Circle // renders dark fill
{
- Size = Size,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ // Slightly inset to prevent bleeding outside the ring
+ Size = circle_size - new Vector2(1),
Alpha = withOuterFill ? 1 : 0,
},
outerGradient = new Circle // renders the outer bright gradient
@@ -88,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Masking = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Size = Size,
+ Size = circle_size,
Child = new KiaiFlash
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs
index a9231b4783..21f2b8f1be 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs
@@ -15,187 +15,175 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
[Test]
public void TestHitAllDrumRoll()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1001),
+ new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1251),
+ new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1501),
+ new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1751),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
- {
- StartTime = hit_time,
- Duration = 1000
- }));
+ }, CreateBeatmap(createDrumRoll(false)));
- AssertJudgementCount(3);
+ AssertJudgementCount(6);
AssertResult(0, HitResult.SmallBonus);
AssertResult(1, HitResult.SmallBonus);
+ AssertResult(2, HitResult.SmallBonus);
+ AssertResult(3, HitResult.SmallBonus);
+ AssertResult(4, HitResult.SmallBonus);
AssertResult(0, HitResult.IgnoreHit);
}
[Test]
public void TestHitSomeDrumRoll()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
- {
- StartTime = hit_time,
- Duration = 1000
- }));
+ }, CreateBeatmap(createDrumRoll(false)));
- AssertJudgementCount(3);
+ AssertJudgementCount(6);
AssertResult(0, HitResult.IgnoreMiss);
- AssertResult(1, HitResult.SmallBonus);
+ AssertResult(1, HitResult.IgnoreMiss);
+ AssertResult(2, HitResult.IgnoreMiss);
+ AssertResult(3, HitResult.IgnoreMiss);
+ AssertResult(4, HitResult.SmallBonus);
AssertResult(0, HitResult.IgnoreHit);
}
[Test]
public void TestHitNoneDrumRoll()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
- }, CreateBeatmap(new DrumRoll
- {
- StartTime = hit_time,
- Duration = 1000
- }));
+ }, CreateBeatmap(createDrumRoll(false)));
- AssertJudgementCount(3);
+ AssertJudgementCount(6);
AssertResult(0, HitResult.IgnoreMiss);
AssertResult(1, HitResult.IgnoreMiss);
+ AssertResult(2, HitResult.IgnoreMiss);
+ AssertResult(3, HitResult.IgnoreMiss);
+ AssertResult(4, HitResult.IgnoreMiss);
AssertResult(0, HitResult.IgnoreHit);
}
[Test]
public void TestHitAllStrongDrumRollWithOneKey()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1001),
+ new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1251),
+ new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1501),
+ new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1751),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
+ }, CreateBeatmap(createDrumRoll(true)));
+
+ AssertJudgementCount(12);
+
+ for (int i = 0; i < 5; i++)
{
- StartTime = hit_time,
- Duration = 1000,
- IsStrong = true
- }));
-
- AssertJudgementCount(6);
-
- AssertResult(0, HitResult.SmallBonus);
- AssertResult(0, HitResult.LargeBonus);
-
- AssertResult(1, HitResult.SmallBonus);
- AssertResult(1, HitResult.LargeBonus);
+ AssertResult(i, HitResult.SmallBonus);
+ AssertResult(i, HitResult.LargeBonus);
+ }
AssertResult(0, HitResult.IgnoreHit);
- AssertResult(2, HitResult.IgnoreHit);
+ AssertResult(5, HitResult.IgnoreHit);
}
[Test]
public void TestHitSomeStrongDrumRollWithOneKey()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
- {
- StartTime = hit_time,
- Duration = 1000,
- IsStrong = true
- }));
+ }, CreateBeatmap(createDrumRoll(true)));
- AssertJudgementCount(6);
+ AssertJudgementCount(12);
AssertResult(0, HitResult.IgnoreMiss);
AssertResult(0, HitResult.IgnoreMiss);
- AssertResult(1, HitResult.SmallBonus);
- AssertResult(1, HitResult.LargeBonus);
+ AssertResult(4, HitResult.SmallBonus);
+ AssertResult(4, HitResult.LargeBonus);
AssertResult(0, HitResult.IgnoreHit);
- AssertResult(2, HitResult.IgnoreHit);
+ AssertResult(5, HitResult.IgnoreHit);
}
[Test]
public void TestHitAllStrongDrumRollWithBothKeys()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(1001),
+ new TaikoReplayFrame(1250, TaikoAction.LeftCentre, TaikoAction.RightCentre),
+ new TaikoReplayFrame(1251),
+ new TaikoReplayFrame(1500, TaikoAction.LeftCentre, TaikoAction.RightCentre),
+ new TaikoReplayFrame(1501),
+ new TaikoReplayFrame(1750, TaikoAction.LeftCentre, TaikoAction.RightCentre),
+ new TaikoReplayFrame(1751),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
+ }, CreateBeatmap(createDrumRoll(true)));
+
+ AssertJudgementCount(12);
+
+ for (int i = 0; i < 5; i++)
{
- StartTime = hit_time,
- Duration = 1000,
- IsStrong = true
- }));
-
- AssertJudgementCount(6);
-
- AssertResult(0, HitResult.SmallBonus);
- AssertResult(0, HitResult.LargeBonus);
-
- AssertResult(1, HitResult.SmallBonus);
- AssertResult(1, HitResult.LargeBonus);
+ AssertResult(i, HitResult.SmallBonus);
+ AssertResult(i, HitResult.LargeBonus);
+ }
AssertResult(0, HitResult.IgnoreHit);
- AssertResult(2, HitResult.IgnoreHit);
+ AssertResult(5, HitResult.IgnoreHit);
}
[Test]
public void TestHitSomeStrongDrumRollWithBothKeys()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
- {
- StartTime = hit_time,
- Duration = 1000,
- IsStrong = true
- }));
+ }, CreateBeatmap(createDrumRoll(true)));
- AssertJudgementCount(6);
+ AssertJudgementCount(12);
AssertResult(0, HitResult.IgnoreMiss);
AssertResult(0, HitResult.IgnoreMiss);
- AssertResult(1, HitResult.SmallBonus);
- AssertResult(1, HitResult.LargeBonus);
+ AssertResult(4, HitResult.SmallBonus);
+ AssertResult(4, HitResult.LargeBonus);
AssertResult(0, HitResult.IgnoreHit);
- AssertResult(2, HitResult.IgnoreHit);
+ AssertResult(5, HitResult.IgnoreHit);
}
+
+ private DrumRoll createDrumRoll(bool strong) => new DrumRoll
+ {
+ StartTime = 1000,
+ Duration = 1000,
+ IsStrong = strong
+ };
}
}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index e298e313df..1c2e7abafe 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -92,6 +92,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}).ToList();
}
+ // TODO: stable makes the last tick of a drumroll non-required when the next object is too close.
+ // This probably needs to be reimplemented:
+ //
+ // List hitobjects = hitObjectManager.hitObjects;
+ // int ind = hitobjects.IndexOf(this);
+ // if (i < hitobjects.Count - 1 && hitobjects[i + 1].HittableStartTime - (EndTime + (int)TickSpacing) <= (int)TickSpacing)
+ // lastTickHittable = false;
+
return converted;
}
@@ -133,7 +141,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = taikoDuration,
- TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4,
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
};
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index 2f4a98bd8f..79d17b4a1f 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Objects.Types;
using System.Threading;
using osu.Framework.Bindables;
@@ -69,6 +67,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
+ TickRate = difficulty.SliderTickRate == 3 ? 3 : 4;
+
tickSpacing = timingPoint.BeatLength / TickRate;
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs
index d7e37899ce..cecb99c690 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
private const double pre_beat_transition_time = 80;
- private const float flash_opacity = 0.3f;
+ private const float kiai_flash_opacity = 0.15f;
private ColourInfo accentColour;
@@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
if (drawableHitObject.State.Value == ArmedState.Idle)
{
flash
- .FadeTo(flash_opacity)
+ .FadeTo(kiai_flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
index bde502bbed..b3833d372c 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
private const double pre_beat_transition_time = 80;
- private const float flash_opacity = 0.3f;
+ private const float kiai_flash_opacity = 0.15f;
[Resolved]
private DrawableHitObject drawableHitObject { get; set; } = null!;
@@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
if (drawableHitObject.State.Value == ArmedState.Idle)
{
flashBox
- .FadeTo(flash_opacity)
+ .FadeTo(kiai_flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index 09130ac57d..fac5e098b9 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -231,7 +231,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override IBeatmap GetBeatmap() => beatmap;
- protected override Texture GetBackground() => throw new NotImplementedException();
+ public override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
diff --git a/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs b/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs
index 295a10ba5b..3d1f7c5b17 100644
--- a/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs
+++ b/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs
@@ -131,7 +131,7 @@ namespace osu.Game.Tests.Editing.Checks
var mock = new Mock();
mock.SetupGet(w => w.Beatmap).Returns(beatmap);
- mock.SetupGet(w => w.Background).Returns(background);
+ mock.Setup(w => w.GetBackground()).Returns(background);
mock.Setup(w => w.GetStream(It.IsAny())).Returns(stream);
return mock;
diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
index 8f4250799e..1523ae7027 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
@@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Background
this.renderer = renderer;
}
- protected override Texture GetBackground() => renderer.CreateTexture(1, 1);
+ public override Texture GetBackground() => renderer.CreateTexture(1, 1);
}
private partial class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
index c7b6d984ed..88b959a2a0 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Diagnostics;
using System.Linq;
@@ -23,8 +21,8 @@ namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene
{
- private BeatDivisorControl beatDivisorControl;
- private BindableBeatDivisor bindableBeatDivisor;
+ private BeatDivisorControl beatDivisorControl = null!;
+ private BindableBeatDivisor bindableBeatDivisor = null!;
private SliderBar tickSliderBar => beatDivisorControl.ChildrenOfType>().Single();
private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType().Single();
@@ -169,9 +167,11 @@ namespace osu.Game.Tests.Visual.Editing
switchPresets(1);
assertPreset(BeatDivisorType.Triplets);
+ assertBeatSnap(6);
switchPresets(1);
assertPreset(BeatDivisorType.Common);
+ assertBeatSnap(4);
switchPresets(-1);
assertPreset(BeatDivisorType.Triplets);
@@ -187,6 +187,7 @@ namespace osu.Game.Tests.Visual.Editing
setDivisorViaInput(15);
assertPreset(BeatDivisorType.Custom, 15);
+ assertBeatSnap(15);
switchBeatSnap(-1);
assertBeatSnap(5);
@@ -196,12 +197,14 @@ namespace osu.Game.Tests.Visual.Editing
setDivisorViaInput(5);
assertPreset(BeatDivisorType.Custom, 15);
+ assertBeatSnap(5);
switchPresets(1);
assertPreset(BeatDivisorType.Common);
switchPresets(-1);
- assertPreset(BeatDivisorType.Triplets);
+ assertPreset(BeatDivisorType.Custom, 15);
+ assertBeatSnap(15);
}
private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () =>
@@ -225,7 +228,7 @@ namespace osu.Game.Tests.Visual.Editing
private void assertPreset(BeatDivisorType type, int? maxDivisor = null)
{
- AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type == type);
+ AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type, () => Is.EqualTo(type));
if (type == BeatDivisorType.Custom)
{
@@ -243,7 +246,7 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.Click(MouseButton.Left);
});
- BeatDivisorControl.CustomDivisorPopover popover = null;
+ BeatDivisorControl.CustomDivisorPopover? popover = null;
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().SingleOrDefault()) != null && popover.IsLoaded);
AddStep($"set divisor to {divisor}", () =>
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
index 4f825e1191..885c00be80 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using System.Threading;
@@ -24,8 +22,8 @@ namespace osu.Game.Tests.Visual.Online
{
private readonly APIUser streamingUser = new APIUser { Id = 2, Username = "Test user" };
- private TestSpectatorClient spectatorClient;
- private CurrentlyPlayingDisplay currentlyPlaying;
+ private TestSpectatorClient spectatorClient = null!;
+ private CurrentlyPlayingDisplay currentlyPlaying = null!;
[SetUpSteps]
public void SetUpSteps()
@@ -88,13 +86,13 @@ namespace osu.Game.Tests.Visual.Online
"pishifat"
};
- protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
+ protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
{
// tests against failed lookups
if (lookup == 13)
- return Task.FromResult(null);
+ return Task.FromResult(null);
- return Task.FromResult(new APIUser
+ return Task.FromResult(new APIUser
{
Id = lookup,
Username = usernames[lookup % usernames.Length],
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
index c2537cff79..379bd838cd 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
@@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -24,10 +23,10 @@ namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneBeatmapMetadataDisplay : OsuTestScene
{
- private BeatmapMetadataDisplay display;
+ private BeatmapMetadataDisplay display = null!;
[Resolved]
- private BeatmapManager manager { get; set; }
+ private BeatmapManager manager { get; set; } = null!;
[Cached(typeof(BeatmapDifficultyCache))]
private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache();
@@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
{
- private TaskCompletionSource calculationBlocker;
+ private TaskCompletionSource? calculationBlocker;
private bool blockCalculation;
@@ -142,10 +141,13 @@ namespace osu.Game.Tests.Visual.SongSelect
}
}
- public override async Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo rulesetInfo = null, IEnumerable mods = null, CancellationToken cancellationToken = default)
+ public override async Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable? mods = null, CancellationToken cancellationToken = default)
{
if (blockCalculation)
+ {
+ Debug.Assert(calculationBlocker != null);
await calculationBlocker.Task.ConfigureAwait(false);
+ }
return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false);
}
diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs
index 5e41392560..12660ed2e1 100644
--- a/osu.Game.Tests/WaveformTestBeatmap.cs
+++ b/osu.Game.Tests/WaveformTestBeatmap.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Tests
protected override IBeatmap GetBeatmap() => beatmap;
- protected override Texture GetBackground() => null;
+ public override Texture GetBackground() => null;
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));
diff --git a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs
new file mode 100644
index 0000000000..acd60b664d
--- /dev/null
+++ b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs
@@ -0,0 +1,95 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.IO.Stores;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Processing;
+
+namespace osu.Game.Beatmaps
+{
+ // Implementation of this class is based off of `MaxDimensionLimitedTextureLoaderStore`.
+ // If issues are found it's worth checking to make sure similar issues exist there.
+ public class BeatmapPanelBackgroundTextureLoaderStore : IResourceStore
+ {
+ // The aspect ratio of SetPanelBackground at its maximum size (very tall window).
+ private const float minimum_display_ratio = 512 / 80f;
+
+ private readonly IResourceStore? textureStore;
+
+ public BeatmapPanelBackgroundTextureLoaderStore(IResourceStore? textureStore)
+ {
+ this.textureStore = textureStore;
+ }
+
+ public void Dispose()
+ {
+ textureStore?.Dispose();
+ }
+
+ public TextureUpload Get(string name)
+ {
+ var textureUpload = textureStore?.Get(name);
+
+ // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
+ if (textureUpload == null)
+ return null!;
+
+ return limitTextureUploadSize(textureUpload);
+ }
+
+ public async Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken())
+ {
+ // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
+ if (textureStore == null)
+ return null!;
+
+ var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false);
+
+ if (textureUpload == null)
+ return null!;
+
+ return await Task.Run(() => limitTextureUploadSize(textureUpload), cancellationToken).ConfigureAwait(false);
+ }
+
+ private TextureUpload limitTextureUploadSize(TextureUpload textureUpload)
+ {
+ var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
+
+ // The original texture upload will no longer be returned or used.
+ textureUpload.Dispose();
+
+ Size size = image.Size();
+
+ // Assume that panel backgrounds are always displayed using `FillMode.Fill`.
+ // Also assume that all backgrounds are wider than they are tall, so the
+ // fill is always going to be based on width.
+ //
+ // We need to include enough height to make this work for all ratio panels are displayed at.
+ int usableHeight = (int)Math.Ceiling(size.Width * 1 / minimum_display_ratio);
+
+ usableHeight = Math.Min(size.Height, usableHeight);
+
+ // Crop the centre region of the background for now.
+ Rectangle cropRectangle = new Rectangle(
+ 0,
+ (size.Height - usableHeight) / 2,
+ size.Width,
+ usableHeight
+ );
+
+ image.Mutate(i => i.Crop(cropRectangle));
+
+ return new TextureUpload(image);
+ }
+
+ public Stream? GetStream(string name) => textureStore?.GetStream(name);
+
+ public IEnumerable GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty();
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
index 767504fcb1..5b9cf6846c 100644
--- a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
+++ b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
@@ -23,8 +23,9 @@ namespace osu.Game.Beatmaps.Drawables
[BackgroundDependencyLoader]
private void load()
{
- if (working.Background != null)
- Texture = working.Background;
+ var background = working.GetBackground();
+ if (background != null)
+ Texture = background;
}
}
}
diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
index 0b390a2ab5..8089d789c1 100644
--- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => new Beatmap();
- protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4");
+ public override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4");
protected override Track GetBeatmapTrack() => GetVirtualTrack();
diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
index 02fcde5257..0b53278ab3 100644
--- a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Beatmaps
}
protected override IBeatmap GetBeatmap() => beatmap;
- protected override Texture GetBackground() => throw new NotImplementedException();
+ public override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
protected internal override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
diff --git a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs
index 22ff7ce8c8..9e79e03785 100644
--- a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs
+++ b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs
@@ -9,13 +9,18 @@ using osu.Game.IO;
namespace osu.Game.Beatmaps
{
- public interface IBeatmapResourceProvider : IStorageResourceProvider
+ internal interface IBeatmapResourceProvider : IStorageResourceProvider
{
///
/// Retrieve a global large texture store, used for loading beatmap backgrounds.
///
TextureStore LargeTextureStore { get; }
+ ///
+ /// Retrieve a global large texture store, used specifically for retrieving cropped beatmap panel backgrounds.
+ ///
+ TextureStore BeatmapPanelTextureStore { get; }
+
///
/// Access a global track store for retrieving beatmap tracks from.
///
diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs
index 0f0e72b0ac..bdfa6bdf6d 100644
--- a/osu.Game/Beatmaps/IWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs
@@ -32,12 +32,12 @@ namespace osu.Game.Beatmaps
///
/// Whether the Beatmap has finished loading.
///
- public bool BeatmapLoaded { get; }
+ bool BeatmapLoaded { get; }
///
/// Whether the Track has finished loading.
///
- public bool TrackLoaded { get; }
+ bool TrackLoaded { get; }
///
/// Retrieves the which this represents.
@@ -47,7 +47,12 @@ namespace osu.Game.Beatmaps
///
/// Retrieves the background for this .
///
- Texture Background { get; }
+ Texture GetBackground();
+
+ ///
+ /// Retrieves a cropped background for this used for display on panels.
+ ///
+ Texture GetPanelBackground();
///
/// Retrieves the for the of this .
@@ -124,12 +129,12 @@ namespace osu.Game.Beatmaps
///
/// Beings loading the contents of this asynchronously.
///
- public void BeginAsyncLoad();
+ void BeginAsyncLoad();
///
/// Cancels the asynchronous loading of the contents of this .
///
- public void CancelAsyncLoad();
+ void CancelAsyncLoad();
///
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 59a71fd80c..25159996f3 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
public Storyboard Storyboard => storyboard.Value;
- public Texture Background => GetBackground(); // Texture uses ref counting, so we want to return a new instance every usage.
-
public ISkin Skin => skin.Value;
private AudioManager audioManager { get; }
@@ -67,7 +65,8 @@ namespace osu.Game.Beatmaps
protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo };
protected abstract IBeatmap GetBeatmap();
- protected abstract Texture GetBackground();
+ public abstract Texture GetBackground();
+ public virtual Texture GetPanelBackground() => GetBackground();
protected abstract Track GetBeatmapTrack();
///
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index ef843909d8..0f3d61f527 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -42,6 +42,7 @@ namespace osu.Game.Beatmaps
private readonly AudioManager audioManager;
private readonly IResourceStore resources;
private readonly LargeTextureStore largeTextureStore;
+ private readonly LargeTextureStore beatmapPanelTextureStore;
private readonly ITrackStore trackStore;
private readonly IResourceStore files;
@@ -58,6 +59,7 @@ namespace osu.Game.Beatmaps
this.host = host;
this.files = files;
largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files));
+ beatmapPanelTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), new BeatmapPanelBackgroundTextureLoaderStore(host?.CreateTextureLoaderStore(files)));
this.trackStore = trackStore;
}
@@ -110,6 +112,7 @@ namespace osu.Game.Beatmaps
#region IResourceStorageProvider
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
+ TextureStore IBeatmapResourceProvider.BeatmapPanelTextureStore => beatmapPanelTextureStore;
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
AudioManager IStorageResourceProvider.AudioManager => audioManager;
@@ -160,7 +163,11 @@ namespace osu.Game.Beatmaps
}
}
- protected override Texture GetBackground()
+ public override Texture GetPanelBackground() => getBackgroundFromStore(resources.BeatmapPanelTextureStore);
+
+ public override Texture GetBackground() => getBackgroundFromStore(resources.LargeTextureStore);
+
+ private Texture getBackgroundFromStore(TextureStore store)
{
if (string.IsNullOrEmpty(Metadata?.BackgroundFile))
return null;
@@ -168,7 +175,7 @@ namespace osu.Game.Beatmaps
try
{
string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile);
- var texture = resources.LargeTextureStore.Get(fileStorePath);
+ var texture = store.Get(fileStorePath);
if (texture == null)
{
diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs
index 276563e163..5e2f0c2128 100644
--- a/osu.Game/Configuration/SessionStatics.cs
+++ b/osu.Game/Configuration/SessionStatics.cs
@@ -6,6 +6,7 @@
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
+using osu.Game.Overlays.Mods;
namespace osu.Game.Configuration
{
@@ -21,6 +22,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LowBatteryNotificationShownOnce, false);
SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
+ SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null);
SetDefault(Static.SeasonalBackgrounds, null);
}
@@ -56,5 +58,11 @@ namespace osu.Game.Configuration
/// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like .
///
LastHoverSoundPlaybackTime,
+
+ ///
+ /// The last playback time in milliseconds of an on/off sample (from ).
+ /// Used to debounce on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods.
+ ///
+ LastModSelectPanelSamplePlaybackTime
}
}
diff --git a/osu.Game/Database/BeatmapLookupCache.cs b/osu.Game/Database/BeatmapLookupCache.cs
index d9bf0138dc..973c25ec4f 100644
--- a/osu.Game/Database/BeatmapLookupCache.cs
+++ b/osu.Game/Database/BeatmapLookupCache.cs
@@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using JetBrains.Annotations;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -21,8 +18,7 @@ namespace osu.Game.Database
/// The beatmap to lookup.
/// An optional cancellation token.
/// The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.
- [ItemCanBeNull]
- public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
+ public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
///
/// Perform an API lookup on the specified beatmaps, populating a model.
@@ -30,10 +26,10 @@ namespace osu.Game.Database
/// The beatmaps to lookup.
/// An optional cancellation token.
/// The populated beatmaps. May include null results for failed retrievals.
- public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
+ public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
protected override GetBeatmapsRequest CreateRequest(IEnumerable ids) => new GetBeatmapsRequest(ids.ToArray());
- protected override IEnumerable RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
+ protected override IEnumerable? RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
}
}
diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs
index 5d1a381f09..e98475efae 100644
--- a/osu.Game/Database/MemoryCachingComponent.cs
+++ b/osu.Game/Database/MemoryCachingComponent.cs
@@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
-using JetBrains.Annotations;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Statistics;
@@ -19,8 +17,9 @@ namespace osu.Game.Database
/// Currently not persisted between game sessions.
///
public abstract partial class MemoryCachingComponent : Component
+ where TLookup : notnull
{
- private readonly ConcurrentDictionary cache = new ConcurrentDictionary();
+ private readonly ConcurrentDictionary cache = new ConcurrentDictionary();
private readonly GlobalStatistic statistics;
@@ -37,12 +36,12 @@ namespace osu.Game.Database
///
/// The lookup to retrieve.
/// An optional to cancel the operation.
- protected async Task GetAsync([NotNull] TLookup lookup, CancellationToken token = default)
+ protected async Task GetAsync(TLookup lookup, CancellationToken token = default)
{
- if (CheckExists(lookup, out TValue performance))
+ if (CheckExists(lookup, out TValue? existing))
{
statistics.Value.HitCount++;
- return performance;
+ return existing;
}
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
@@ -73,7 +72,7 @@ namespace osu.Game.Database
statistics.Value.Usage = cache.Count;
}
- protected bool CheckExists([NotNull] TLookup lookup, out TValue value) =>
+ protected bool CheckExists(TLookup lookup, [MaybeNullWhen(false)] out TValue value) =>
cache.TryGetValue(lookup, out value);
///
@@ -82,7 +81,7 @@ namespace osu.Game.Database
/// The lookup to retrieve.
/// An optional to cancel the operation.
/// The computed value.
- protected abstract Task ComputeValueAsync(TLookup lookup, CancellationToken token = default);
+ protected abstract Task ComputeValueAsync(TLookup lookup, CancellationToken token = default);
private class MemoryCachingStatistics
{
diff --git a/osu.Game/Database/OnlineLookupCache.cs b/osu.Game/Database/OnlineLookupCache.cs
index d9b37e2f29..3b54804fec 100644
--- a/osu.Game/Database/OnlineLookupCache.cs
+++ b/osu.Game/Database/OnlineLookupCache.cs
@@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Game.Online.API;
@@ -21,7 +18,7 @@ namespace osu.Game.Database
where TRequest : APIRequest
{
[Resolved]
- private IAPIProvider api { get; set; }
+ private IAPIProvider api { get; set; } = null!;
///
/// Creates an to retrieve the values for a given collection of s.
@@ -32,8 +29,7 @@ namespace osu.Game.Database
///
/// Retrieves a list of s from a successful created by .
///
- [CanBeNull]
- protected abstract IEnumerable RetrieveResults(TRequest request);
+ protected abstract IEnumerable? RetrieveResults(TRequest request);
///
/// Perform a lookup using the specified , populating a .
@@ -41,8 +37,7 @@ namespace osu.Game.Database
/// The ID to lookup.
/// An optional cancellation token.
/// The populated , or null if the value does not exist or the request could not be satisfied.
- [ItemCanBeNull]
- protected Task LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
+ protected Task LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
///
/// Perform an API lookup on the specified , populating a .
@@ -50,9 +45,9 @@ namespace osu.Game.Database
/// The IDs to lookup.
/// An optional cancellation token.
/// The populated values. May include null results for failed retrievals.
- protected Task LookupAsync(TLookup[] ids, CancellationToken token = default)
+ protected Task LookupAsync(TLookup[] ids, CancellationToken token = default)
{
- var lookupTasks = new List>();
+ var lookupTasks = new List>();
foreach (var id in ids)
{
@@ -69,18 +64,18 @@ namespace osu.Game.Database
}
// cannot be sealed due to test usages (see TestUserLookupCache).
- protected override async Task ComputeValueAsync(TLookup lookup, CancellationToken token = default)
+ protected override async Task ComputeValueAsync(TLookup lookup, CancellationToken token = default)
=> await queryValue(lookup).ConfigureAwait(false);
- private readonly Queue<(TLookup id, TaskCompletionSource)> pendingTasks = new Queue<(TLookup, TaskCompletionSource)>();
- private Task pendingRequestTask;
+ private readonly Queue<(TLookup id, TaskCompletionSource)> pendingTasks = new Queue<(TLookup, TaskCompletionSource)>();
+ private Task? pendingRequestTask;
private readonly object taskAssignmentLock = new object();
- private Task queryValue(TLookup id)
+ private Task queryValue(TLookup id)
{
lock (taskAssignmentLock)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource();
// Add to the queue.
pendingTasks.Enqueue((id, tcs));
@@ -96,14 +91,14 @@ namespace osu.Game.Database
private async Task performLookup()
{
// contains at most 50 unique IDs from tasks, which is used to perform the lookup.
- var nextTaskBatch = new Dictionary>>();
+ var nextTaskBatch = new Dictionary>>();
// Grab at most 50 unique IDs from the queue.
lock (taskAssignmentLock)
{
while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50)
{
- (TLookup id, TaskCompletionSource task) next = pendingTasks.Dequeue();
+ (TLookup id, TaskCompletionSource task) next = pendingTasks.Dequeue();
// Perform a secondary check for existence, in case the value was queried in a previous batch.
if (CheckExists(next.id, out var existing))
@@ -113,7 +108,7 @@ namespace osu.Game.Database
if (nextTaskBatch.TryGetValue(next.id, out var tasks))
tasks.Add(next.task);
else
- nextTaskBatch[next.id] = new List> { next.task };
+ nextTaskBatch[next.id] = new List> { next.task };
}
}
}
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index f4c6c802f1..63ab18db8c 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -22,12 +22,15 @@ using osu.Framework.Statistics;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Extensions;
using osu.Game.Input.Bindings;
+using osu.Game.IO.Legacy;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
+using osu.Game.Scoring.Legacy;
using osu.Game.Skinning;
using Realms;
using Realms.Exceptions;
@@ -72,8 +75,9 @@ namespace osu.Game.Database
/// 25 2022-09-18 Remove skins to add with new naming.
/// 26 2023-02-05 Added BeatmapHash to ScoreInfo.
/// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo.
+ /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files.
///
- private const int schema_version = 27;
+ private const int schema_version = 28;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
@@ -880,6 +884,7 @@ namespace osu.Game.Database
break;
case 26:
+ {
// Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap.
var scores = migration.NewRealm.All();
@@ -887,6 +892,44 @@ namespace osu.Game.Database
score.BeatmapHash = score.BeatmapInfo.Hash;
break;
+ }
+
+ case 28:
+ {
+ var files = new RealmFileStore(this, storage);
+ var scores = migration.NewRealm.All();
+
+ foreach (var score in scores)
+ {
+ string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath();
+ if (replayFilename == null)
+ continue;
+
+ try
+ {
+ using (var stream = files.Store.GetStream(replayFilename))
+ {
+ if (stream == null)
+ continue;
+
+ // Trimmed down logic from LegacyScoreDecoder to extract the version from replays.
+ using (SerializationReader sr = new SerializationReader(stream))
+ {
+ sr.ReadByte(); // Ruleset.
+ int version = sr.ReadInt32();
+ if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
+ score.IsLegacyScore = true;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database);
+ }
+ }
+
+ break;
+ }
}
}
diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs
index b1609fbf7b..e581d5ce82 100644
--- a/osu.Game/Database/UserLookupCache.cs
+++ b/osu.Game/Database/UserLookupCache.cs
@@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using JetBrains.Annotations;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -21,8 +18,7 @@ namespace osu.Game.Database
/// The user to lookup.
/// An optional cancellation token.
/// The populated user, or null if the user does not exist or the request could not be satisfied.
- [ItemCanBeNull]
- public Task GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
+ public Task GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
///
/// Perform an API lookup on the specified users, populating a model.
@@ -30,10 +26,10 @@ namespace osu.Game.Database
/// The users to lookup.
/// An optional cancellation token.
/// The populated users. May include null results for failed retrievals.
- public Task GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
+ public Task GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
protected override GetUsersRequest CreateRequest(IEnumerable ids) => new GetUsersRequest(ids.ToArray());
- protected override IEnumerable RetrieveResults(GetUsersRequest request) => request.Response?.Users;
+ protected override IEnumerable? RetrieveResults(GetUsersRequest request) => request.Response?.Users;
}
}
diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs
index b79eb4927f..3ace67f410 100644
--- a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs
+++ b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Graphics.Backgrounds
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
- Sprite.Texture = Beatmap?.Background ?? textures.Get(fallbackTextureName);
+ Sprite.Texture = Beatmap?.GetBackground() ?? textures.Get(fallbackTextureName);
}
public override bool Equals(Background other)
diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs
index b57bb215aa..6f7e9c07d2 100644
--- a/osu.Game/Online/API/Requests/GetUsersRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUsersRequest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
namespace osu.Game.Online.API.Requests
diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
index 1d496cc636..cce633d46a 100644
--- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Online.Rooms
{
var beatmap = task.GetResultSafely();
- if (SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID)
+ if (beatmap != null && SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID)
{
selectedBeatmap = beatmap;
beginTracking();
diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs
index 5d9f616e5f..fe42cc0abf 100644
--- a/osu.Game/Overlays/Mods/ModColumn.cs
+++ b/osu.Game/Overlays/Mods/ModColumn.cs
@@ -176,7 +176,7 @@ namespace osu.Game.Overlays.Mods
dequeuedAction();
// each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements).
- selectionDelay = Math.Max(30, selectionDelay * 0.8f);
+ selectionDelay = Math.Max(ModSelectPanel.SAMPLE_PLAYBACK_DELAY, selectionDelay * 0.8f);
lastSelection = Time.Current;
}
else
diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs
index 81285833bd..be01d239b6 100644
--- a/osu.Game/Overlays/Mods/ModSelectPanel.cs
+++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs
@@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Audio;
+using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -45,6 +46,8 @@ namespace osu.Game.Overlays.Mods
public const float CORNER_RADIUS = 7;
public const float HEIGHT = 42;
+ public const double SAMPLE_PLAYBACK_DELAY = 30;
+
protected virtual float IdleSwitchWidth => 14;
protected virtual float ExpandedSwitchWidth => 30;
protected virtual Colour4 BackgroundColour => Active.Value ? AccentColour.Darken(0.3f) : ColourProvider.Background3;
@@ -69,6 +72,8 @@ namespace osu.Game.Overlays.Mods
private Sample? sampleOff;
private Sample? sampleOn;
+ private Bindable lastPlaybackTime = null!;
+
protected ModSelectPanel()
{
RelativeSizeAxes = Axes.X;
@@ -163,13 +168,15 @@ namespace osu.Game.Overlays.Mods
protected abstract void Deselect();
[BackgroundDependencyLoader]
- private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler)
+ private void load(AudioManager audio, SessionStatics statics, ISamplePlaybackDisabler? samplePlaybackDisabler)
{
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
if (samplePlaybackDisabler != null)
((IBindable)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
+
+ lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime);
}
protected sealed override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
@@ -192,10 +199,17 @@ namespace osu.Game.Overlays.Mods
if (samplePlaybackDisabled.Value)
return;
- if (Active.Value)
- sampleOn?.Play();
- else
- sampleOff?.Play();
+ bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY;
+
+ if (enoughTimePassedSinceLastPlayback)
+ {
+ if (Active.Value)
+ sampleOn?.Play();
+ else
+ sampleOff?.Play();
+
+ lastPlaybackTime.Value = Time.Current;
+ }
}
protected override bool OnHover(HoverEvent e)
diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index 66fb3571ba..e3e3b4bd80 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -415,7 +415,7 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
- sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4");
+ sprite.Texture = beatmap?.GetBackground() ?? textures.Get(@"Backgrounds/bg4");
}
}
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs
index 23fa28e7bc..8c3a5c026d 100644
--- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs
+++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Edit.Checks
if (backgroundFile == null)
yield break;
- var texture = context.WorkingBeatmap.Background;
+ var texture = context.WorkingBeatmap.GetBackground();
if (texture == null)
yield break;
diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
index a8972775de..817e8bd5fe 100644
--- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
@@ -125,6 +125,7 @@ namespace osu.Game.Rulesets.Edit
if (currentSnap > DistanceSpacingMultiplier.MinValue)
{
currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value
+ && !DistanceSpacingMultiplier.Disabled
&& !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2);
currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x";
currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)";
@@ -141,28 +142,31 @@ namespace osu.Game.Rulesets.Edit
{
base.LoadComplete();
- if (!DistanceSpacingMultiplier.Disabled)
+ if (DistanceSpacingMultiplier.Disabled)
{
- DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing;
- DistanceSpacingMultiplier.BindValueChanged(multiplier =>
- {
- distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
- distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})";
-
- if (multiplier.NewValue != multiplier.OldValue)
- onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
-
- EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
- }, true);
-
- // Manual binding to handle enabling distance spacing when the slider is interacted with.
- distanceSpacingSlider.Current.BindValueChanged(spacing =>
- {
- DistanceSpacingMultiplier.Value = spacing.NewValue;
- DistanceSnapToggle.Value = TernaryState.True;
- });
- DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
+ distanceSpacingSlider.Hide();
+ return;
}
+
+ DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing;
+ DistanceSpacingMultiplier.BindValueChanged(multiplier =>
+ {
+ distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
+ distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})";
+
+ if (multiplier.NewValue != multiplier.OldValue)
+ onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
+
+ EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
+ }, true);
+
+ // Manual binding to handle enabling distance spacing when the slider is interacted with.
+ distanceSpacingSlider.Current.BindValueChanged(spacing =>
+ {
+ DistanceSpacingMultiplier.Value = spacing.NewValue;
+ DistanceSnapToggle.Value = TernaryState.True;
+ });
+ DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
}
protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
index 9b145ad56e..c6461840aa 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
@@ -46,6 +46,9 @@ namespace osu.Game.Scoring.Legacy
score.ScoreInfo = scoreInfo;
int version = sr.ReadInt32();
+
+ scoreInfo.IsLegacyScore = version < LegacyScoreEncoder.FIRST_LAZER_VERSION;
+
string beatmapHash = sr.ReadString();
workingBeatmap = GetBeatmap(beatmapHash);
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
index a78ae24da2..87e1e79f87 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
@@ -28,9 +28,10 @@ namespace osu.Game.Scoring.Legacy
///
///
/// - 30000001: Appends to the end of scores.
+ /// - 30000002: Score stored to replay calculated using the Score V2 algorithm.
///
///
- public const int LATEST_VERSION = 30000001;
+ public const int LATEST_VERSION = 30000002;
///
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
index 84bf6d15f6..52dec20b32 100644
--- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
+++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
@@ -16,7 +16,13 @@ namespace osu.Game.Scoring.Legacy
=> getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics);
public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode)
- => getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics);
+ {
+ // Temporary to not scale stable scores that are already in the XX-millions with the classic scoring mode.
+ if (scoreInfo.IsLegacyScore)
+ return scoreInfo.TotalScore;
+
+ return getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics);
+ }
private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary maximumStatistics)
{
diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs
index f69c1b9385..1c24cfbc85 100644
--- a/osu.Game/Scoring/ScoreImporter.cs
+++ b/osu.Game/Scoring/ScoreImporter.cs
@@ -146,16 +146,48 @@ namespace osu.Game.Scoring
#pragma warning restore CS0618
}
+ // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores).
+ private readonly Dictionary usernameLookupCache = new Dictionary();
+
protected override void PostImport(ScoreInfo model, Realm realm, ImportParameters parameters)
{
base.PostImport(model, realm, parameters);
- var userRequest = new GetUserRequest(model.RealmUser.Username);
+ populateUserDetails(model);
+ }
+
+ ///
+ /// Legacy replays only store a username.
+ /// This will populate a user ID during import.
+ ///
+ private void populateUserDetails(ScoreInfo model)
+ {
+ string username = model.RealmUser.Username;
+
+ if (usernameLookupCache.TryGetValue(username, out var existing))
+ {
+ model.User = existing;
+ return;
+ }
+
+ var userRequest = new GetUserRequest(username);
api.Perform(userRequest);
if (userRequest.Response is APIUser user)
+ {
+ usernameLookupCache.TryAdd(username, new APIUser
+ {
+ // Because this is a permanent cache, let's only store the pieces we're interested in,
+ // rather than the full API response. If we start to store more than these three fields
+ // in realm, this should be undone.
+ Id = user.Id,
+ Username = user.Username,
+ CountryCode = user.CountryCode,
+ });
+
model.User = user;
+ }
}
}
}
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index e084c45de0..d56338c6a4 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -181,8 +181,7 @@ namespace osu.Game.Scoring
///
/// Whether this represents a legacy (osu!stable) score.
///
- [Ignored]
- public bool IsLegacyScore => Mods.OfType().Any();
+ public bool IsLegacyScore { get; set; }
private Dictionary? statistics;
diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs
index 17a0c0ea6a..bdbcfe4efe 100644
--- a/osu.Game/Scoring/ScorePerformanceCache.cs
+++ b/osu.Game/Scoring/ScorePerformanceCache.cs
@@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Threading;
using System.Threading.Tasks;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Database;
@@ -21,7 +18,7 @@ namespace osu.Game.Scoring
public partial class ScorePerformanceCache : MemoryCachingComponent
{
[Resolved]
- private BeatmapDifficultyCache difficultyCache { get; set; }
+ private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
protected override bool CacheNullValues => false;
@@ -30,10 +27,10 @@ namespace osu.Game.Scoring
///
/// The score to do the calculation on.
/// An optional to cancel the operation.
- public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) =>
+ public Task CalculatePerformanceAsync(ScoreInfo score, CancellationToken token = default) =>
GetAsync(new PerformanceCacheLookup(score), token);
- protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default)
+ protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default)
{
var score = lookup.ScoreInfo;
diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
index 5a1fbbee1e..f7159f8670 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Diagnostics;
using System.Linq;
@@ -33,6 +31,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
public partial class BeatDivisorControl : CompositeDrawable, IKeyBindingHandler
{
+ private int? lastCustomDivisor;
+
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
public BeatDivisorControl(BindableBeatDivisor beatDivisor)
@@ -186,29 +186,46 @@ namespace osu.Game.Screens.Edit.Compose.Components
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ beatDivisor.ValidDivisors.BindValueChanged(valid =>
+ {
+ if (valid.NewValue.Type == BeatDivisorType.Custom)
+ lastCustomDivisor = valid.NewValue.Presets.Last();
+ }, true);
+ }
+
private void cycleDivisorType(int direction)
{
- Debug.Assert(Math.Abs(direction) == 1);
- int nextDivisorType = (int)beatDivisor.ValidDivisors.Value.Type + direction;
- if (nextDivisorType > (int)BeatDivisorType.Triplets)
- nextDivisorType = (int)BeatDivisorType.Common;
- else if (nextDivisorType < (int)BeatDivisorType.Common)
- nextDivisorType = (int)BeatDivisorType.Triplets;
+ int totalTypes = Enum.GetValues().Length;
+ BeatDivisorType currentType = beatDivisor.ValidDivisors.Value.Type;
- switch ((BeatDivisorType)nextDivisorType)
+ Debug.Assert(Math.Abs(direction) == 1);
+
+ cycleOnce();
+
+ if (lastCustomDivisor == null && currentType == BeatDivisorType.Custom)
+ cycleOnce();
+
+ switch (currentType)
{
case BeatDivisorType.Common:
- beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.COMMON;
+ beatDivisor.SetArbitraryDivisor(4);
break;
case BeatDivisorType.Triplets:
- beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS;
+ beatDivisor.SetArbitraryDivisor(6);
break;
case BeatDivisorType.Custom:
- beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.Custom(beatDivisor.ValidDivisors.Value.Presets.Max());
+ Debug.Assert(lastCustomDivisor != null);
+ beatDivisor.SetArbitraryDivisor(lastCustomDivisor.Value);
break;
}
+
+ void cycleOnce() => currentType = (BeatDivisorType)(((int)currentType + totalTypes + direction) % totalTypes);
}
protected override bool OnKeyDown(KeyDownEvent e)
@@ -326,12 +343,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.LoadComplete();
BeatDivisor.BindValueChanged(_ => updateState(), true);
- divisorTextBox.OnCommit += (_, _) => setPresets();
+ divisorTextBox.OnCommit += (_, _) => setPresetsFromTextBoxEntry();
Schedule(() => GetContainingInputManager().ChangeFocus(divisorTextBox));
}
- private void setPresets()
+ private void setPresetsFromTextBoxEntry()
{
if (!int.TryParse(divisorTextBox.Text, out int divisor) || divisor < 1 || divisor > 64)
{
@@ -394,10 +411,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
private partial class TickSliderBar : SliderBar
{
- private Marker marker;
+ private Marker marker = null!;
[Resolved]
- private OsuColour colours { get; set; }
+ private OsuColour colours { get; set; } = null!;
private readonly BindableBeatDivisor beatDivisor;
@@ -539,7 +556,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private partial class Marker : CompositeDrawable
{
[Resolved]
- private OverlayColourProvider colourProvider { get; set; }
+ private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs
index 84cfac8f65..b8cbff047e 100644
--- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs
+++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Screens.Edit
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 5 },
},
- new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both }
+ new BeatDivisorControl(this.beatDivisor) { RelativeSizeAxes = Axes.Both }
},
},
RowDimensions = new[]
diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs
index 2cf823ca0c..565379f391 100644
--- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs
+++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs
@@ -204,7 +204,7 @@ namespace osu.Game.Screens.Edit
protected override IBeatmap GetBeatmap() => beatmap;
- protected override Texture GetBackground() => throw new NotImplementedException();
+ public override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
diff --git a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs b/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs
index 997ba6b639..6b06eaee1e 100644
--- a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
if (Beatmap?.BeatmapSet is IBeatmapSetOnlineInfo online)
texture = textures.Get(online.Covers.Cover);
- Sprite.Texture = texture ?? beatmaps.DefaultBeatmap.Background;
+ Sprite.Texture = texture ?? beatmaps.DefaultBeatmap.GetBackground();
}
public override bool Equals(Background? other)
diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
index a152f4be19..66aa3d9cc0 100644
--- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
+++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
@@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play
new Sprite
{
RelativeSizeAxes = Axes.Both,
- Texture = beatmap.Background,
+ Texture = beatmap.GetBackground(),
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
FillMode = FillMode.Fill,
diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs
index 4f37c215e9..82f116b4ae 100644
--- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs
+++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs
@@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.HUD
protected override IBeatmap GetBeatmap() => gameplayBeatmap;
- protected override Texture GetBackground() => throw new NotImplementedException();
+ public override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
index 45b2c1b13c..064d2071ce 100644
--- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
+++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
@@ -42,7 +42,8 @@ namespace osu.Game.Screens.Play.HUD
//CollectionSettings = new CollectionSettings(),
//DiscussionSettings = new DiscussionSettings(),
PlaybackSettings = new PlaybackSettings { Expanded = { Value = false } },
- VisualSettings = new VisualSettings { Expanded = { Value = false } }
+ VisualSettings = new VisualSettings { Expanded = { Value = false } },
+ new AudioSettings { Expanded = { Value = false } }
}
};
}
diff --git a/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs b/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs
index 6f13a34bfc..b8729b7174 100644
--- a/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs
+++ b/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs
@@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.Drawables;
using osuTK;
using osuTK.Graphics;
@@ -21,7 +23,7 @@ namespace osu.Game.Screens.Select.Carousel
Children = new Drawable[]
{
- new BeatmapBackgroundSprite(working)
+ new PanelBeatmapBackground(working)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
@@ -68,5 +70,23 @@ namespace osu.Game.Screens.Select.Carousel
},
};
}
+
+ public partial class PanelBeatmapBackground : Sprite
+ {
+ private readonly IWorkingBeatmap working;
+
+ public PanelBeatmapBackground(IWorkingBeatmap working)
+ {
+ ArgumentNullException.ThrowIfNull(working);
+
+ this.working = working;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Texture = working.GetPanelBackground();
+ }
+ }
}
}
diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
index 79f629ce49..b57b0daa1b 100644
--- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
+++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
@@ -232,7 +232,7 @@ namespace osu.Game.Tests.Beatmaps
protected override IBeatmap GetBeatmap() => beatmap;
- protected override Texture GetBackground() => throw new NotImplementedException();
+ public override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
index 7d2aa99dbe..ba6d9ca8b5 100644
--- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
+++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Beatmaps
public override Stream? GetStream(string storagePath) => null;
- protected override Texture? GetBackground() => null;
+ public override Texture? GetBackground() => null;
protected override Track? GetBeatmapTrack() => null;
}
diff --git a/osu.Game/Tests/Visual/TestUserLookupCache.cs b/osu.Game/Tests/Visual/TestUserLookupCache.cs
index a3028f1a34..261e0fa75c 100644
--- a/osu.Game/Tests/Visual/TestUserLookupCache.cs
+++ b/osu.Game/Tests/Visual/TestUserLookupCache.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Threading;
using System.Threading.Tasks;
using osu.Game.Database;
@@ -18,12 +16,12 @@ namespace osu.Game.Tests.Visual
///
public const int UNRESOLVED_USER_ID = -1;
- protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
+ protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
{
if (lookup == UNRESOLVED_USER_ID)
- return Task.FromResult((APIUser)null);
+ return Task.FromResult(null);
- return Task.FromResult(new APIUser
+ return Task.FromResult(new APIUser
{
Id = lookup,
Username = $"User {lookup}"
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 8a941ca6c1..b2faa7dfc2 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 1dcece7741..9aafec6c50 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -16,6 +16,6 @@
iossimulator-x64
-
+