1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 11:42:56 +08:00

Merge branch 'master' into improve-song-select-enter-performance

This commit is contained in:
Salman Ahmed 2023-06-12 13:13:59 +03:00 committed by GitHub
commit 7269b4807e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 545 additions and 260 deletions

View File

@ -11,7 +11,7 @@
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger> <AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.531.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.608.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" /> <AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />

View File

@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{ {
return 200000 * comboProgress return 10000 * comboProgress
+ 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress + 990000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ bonusPortion; + bonusPortion;
} }

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("place first object", () => InputManager.Click(MouseButton.Left)); 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)); 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("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)); 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("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", () => AddAssert("object 3 snapped to 1", () =>
{ {
@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return Precision.AlmostEquals(first.EndPosition, third.Position); 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", () => AddAssert("object 2 snapped to 1", () =>
{ {

View File

@ -1,22 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
public partial class TestSceneOsuModAutoplay : OsuModTestScene public partial class TestSceneOsuModAutoplay : OsuModTestScene
{ {
protected override bool AllowFail => true;
[Test] [Test]
public void TestCursorPositionStoredToJudgement() public void TestCursorPositionStoredToJudgement()
{ {
@ -44,6 +51,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
FinalRate = { Value = 1.3 } 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<HitObject>
{
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) private void runSpmTest(Mod mod)
{ {
SpinnerSpmCalculator? spmCalculator = null; SpinnerSpmCalculator? spmCalculator = null;

View File

@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Osu.Edit
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
float snapRadius = 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; playfield.GamefieldToScreenSpace(Vector2.Zero).X;
foreach (var b in blueprints) foreach (var b in blueprints)

View File

@ -75,18 +75,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both };
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
shakeContainer = new ShakeContainer shakeContainer = new ShakeContainer
{ {
ShakeDuration = 30, ShakeDuration = 30,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new[]
{ {
Body = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), Body = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both }, // proxied here so that the tail is drawn under repeats/ticks - legacy skins rely on this
tailContainer.CreateProxy(),
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both }, tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container<DrawableSliderRepeat> { 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. // slider head is not included in shake as it handles hit detection, and handles its own shaking.

View File

@ -48,21 +48,26 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private Bindable<bool> configHitLighting = null!; private Bindable<bool> configHitLighting = null!;
private static readonly Vector2 circle_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
[Resolved] [Resolved]
private DrawableHitObject drawableObject { get; set; } = null!; private DrawableHitObject drawableObject { get; set; } = null!;
public ArgonMainCirclePiece(bool withOuterFill) public ArgonMainCirclePiece(bool withOuterFill)
{ {
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Size = circle_size;
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
InternalChildren = new Drawable[] 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, Alpha = withOuterFill ? 1 : 0,
}, },
outerGradient = new Circle // renders the outer bright gradient outerGradient = new Circle // renders the outer bright gradient
@ -88,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Masking = true, Masking = true,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = Size, Size = circle_size,
Child = new KiaiFlash Child = new KiaiFlash
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -15,187 +15,175 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
[Test] [Test]
public void TestHitAllDrumRoll() public void TestHitAllDrumRoll()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre), new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1001), 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(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(false)));
{
StartTime = hit_time,
Duration = 1000
}));
AssertJudgementCount(3); AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.SmallBonus); AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
AssertResult<DrumRollTick>(2, HitResult.SmallBonus);
AssertResult<DrumRollTick>(3, HitResult.SmallBonus);
AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitSomeDrumRoll() public void TestHitSomeDrumRoll()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(false)));
{
StartTime = hit_time,
Duration = 1000
}));
AssertJudgementCount(3); AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(1, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(2, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(3, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitNoneDrumRoll() public void TestHitNoneDrumRoll()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(false)));
{
StartTime = hit_time,
Duration = 1000
}));
AssertJudgementCount(3); AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(1, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(2, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(3, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(4, HitResult.IgnoreMiss);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitAllStrongDrumRollWithOneKey() public void TestHitAllStrongDrumRollWithOneKey()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre), new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1001), 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(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
AssertJudgementCount(12);
for (int i = 0; i < 5; i++)
{ {
StartTime = hit_time, AssertResult<DrumRollTick>(i, HitResult.SmallBonus);
Duration = 1000, AssertResult<StrongNestedHitObject>(i, HitResult.LargeBonus);
IsStrong = true }
}));
AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(0, HitResult.LargeBonus);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitSomeStrongDrumRollWithOneKey() public void TestHitSomeStrongDrumRollWithOneKey()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
{
StartTime = hit_time,
Duration = 1000,
IsStrong = true
}));
AssertJudgementCount(6); AssertJudgementCount(12);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss); AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus); AssertResult<StrongNestedHitObject>(4, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitAllStrongDrumRollWithBothKeys() public void TestHitAllStrongDrumRollWithBothKeys()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre), new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(1001), 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(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
AssertJudgementCount(12);
for (int i = 0; i < 5; i++)
{ {
StartTime = hit_time, AssertResult<DrumRollTick>(i, HitResult.SmallBonus);
Duration = 1000, AssertResult<StrongNestedHitObject>(i, HitResult.LargeBonus);
IsStrong = true }
}));
AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(0, HitResult.LargeBonus);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitSomeStrongDrumRollWithBothKeys() public void TestHitSomeStrongDrumRollWithBothKeys()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
{
StartTime = hit_time,
Duration = 1000,
IsStrong = true
}));
AssertJudgementCount(6); AssertJudgementCount(12);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss); AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus); AssertResult<StrongNestedHitObject>(4, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
private DrumRoll createDrumRoll(bool strong) => new DrumRoll
{
StartTime = 1000,
Duration = 1000,
IsStrong = strong
};
} }
} }

View File

@ -92,6 +92,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}).ToList(); }).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<HitObject> 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; return converted;
} }
@ -133,7 +141,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
Duration = taikoDuration, Duration = taikoDuration,
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4,
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1 SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
}; };
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System.Threading; using System.Threading;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -69,6 +67,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity; double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength; Velocity = scoringDistance / timingPoint.BeatLength;
TickRate = difficulty.SliderTickRate == 3 ? 3 : 4;
tickSpacing = timingPoint.BeatLength / TickRate; tickSpacing = timingPoint.BeatLength / TickRate;
} }

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
private const double pre_beat_transition_time = 80; 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; private ColourInfo accentColour;
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
if (drawableHitObject.State.Value == ArmedState.Idle) if (drawableHitObject.State.Value == ArmedState.Idle)
{ {
flash flash
.FadeTo(flash_opacity) .FadeTo(kiai_flash_opacity)
.Then() .Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
} }

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
private const double pre_beat_transition_time = 80; private const double pre_beat_transition_time = 80;
private const float flash_opacity = 0.3f; private const float kiai_flash_opacity = 0.15f;
[Resolved] [Resolved]
private DrawableHitObject drawableHitObject { get; set; } = null!; private DrawableHitObject drawableHitObject { get; set; } = null!;
@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
if (drawableHitObject.State.Value == ArmedState.Idle) if (drawableHitObject.State.Value == ArmedState.Idle)
{ {
flashBox flashBox
.FadeTo(flash_opacity) .FadeTo(kiai_flash_opacity)
.Then() .Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
} }

View File

@ -231,7 +231,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override IBeatmap GetBeatmap() => beatmap; 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 override Track GetBeatmapTrack() => throw new NotImplementedException();

View File

@ -131,7 +131,7 @@ namespace osu.Game.Tests.Editing.Checks
var mock = new Mock<IWorkingBeatmap>(); var mock = new Mock<IWorkingBeatmap>();
mock.SetupGet(w => w.Beatmap).Returns(beatmap); 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<string>())).Returns(stream); mock.Setup(w => w.GetStream(It.IsAny<string>())).Returns(stream);
return mock; return mock;

View File

@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Background
this.renderer = renderer; this.renderer = renderer;
} }
protected override Texture GetBackground() => renderer.CreateTexture(1, 1); public override Texture GetBackground() => renderer.CreateTexture(1, 1);
} }
private partial class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap private partial class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -23,8 +21,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene
{ {
private BeatDivisorControl beatDivisorControl; private BeatDivisorControl beatDivisorControl = null!;
private BindableBeatDivisor bindableBeatDivisor; private BindableBeatDivisor bindableBeatDivisor = null!;
private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single(); private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single();
private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType<Triangle>().Single(); private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType<Triangle>().Single();
@ -169,9 +167,11 @@ namespace osu.Game.Tests.Visual.Editing
switchPresets(1); switchPresets(1);
assertPreset(BeatDivisorType.Triplets); assertPreset(BeatDivisorType.Triplets);
assertBeatSnap(6);
switchPresets(1); switchPresets(1);
assertPreset(BeatDivisorType.Common); assertPreset(BeatDivisorType.Common);
assertBeatSnap(4);
switchPresets(-1); switchPresets(-1);
assertPreset(BeatDivisorType.Triplets); assertPreset(BeatDivisorType.Triplets);
@ -187,6 +187,7 @@ namespace osu.Game.Tests.Visual.Editing
setDivisorViaInput(15); setDivisorViaInput(15);
assertPreset(BeatDivisorType.Custom, 15); assertPreset(BeatDivisorType.Custom, 15);
assertBeatSnap(15);
switchBeatSnap(-1); switchBeatSnap(-1);
assertBeatSnap(5); assertBeatSnap(5);
@ -196,12 +197,14 @@ namespace osu.Game.Tests.Visual.Editing
setDivisorViaInput(5); setDivisorViaInput(5);
assertPreset(BeatDivisorType.Custom, 15); assertPreset(BeatDivisorType.Custom, 15);
assertBeatSnap(5);
switchPresets(1); switchPresets(1);
assertPreset(BeatDivisorType.Common); assertPreset(BeatDivisorType.Common);
switchPresets(-1); switchPresets(-1);
assertPreset(BeatDivisorType.Triplets); assertPreset(BeatDivisorType.Custom, 15);
assertBeatSnap(15);
} }
private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () => 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) 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) if (type == BeatDivisorType.Custom)
{ {
@ -243,7 +246,7 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
BeatDivisorControl.CustomDivisorPopover popover = null; BeatDivisorControl.CustomDivisorPopover? popover = null;
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<BeatDivisorControl.CustomDivisorPopover>().SingleOrDefault()) != null && popover.IsLoaded); AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<BeatDivisorControl.CustomDivisorPopover>().SingleOrDefault()) != null && popover.IsLoaded);
AddStep($"set divisor to {divisor}", () => AddStep($"set divisor to {divisor}", () =>
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Linq; using System.Linq;
using System.Threading; 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 readonly APIUser streamingUser = new APIUser { Id = 2, Username = "Test user" };
private TestSpectatorClient spectatorClient; private TestSpectatorClient spectatorClient = null!;
private CurrentlyPlayingDisplay currentlyPlaying; private CurrentlyPlayingDisplay currentlyPlaying = null!;
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
@ -88,13 +86,13 @@ namespace osu.Game.Tests.Visual.Online
"pishifat" "pishifat"
}; };
protected override Task<APIUser> ComputeValueAsync(int lookup, CancellationToken token = default) protected override Task<APIUser?> ComputeValueAsync(int lookup, CancellationToken token = default)
{ {
// tests against failed lookups // tests against failed lookups
if (lookup == 13) if (lookup == 13)
return Task.FromResult<APIUser>(null); return Task.FromResult<APIUser?>(null);
return Task.FromResult(new APIUser return Task.FromResult<APIUser?>(new APIUser
{ {
Id = lookup, Id = lookup,
Username = usernames[lookup % usernames.Length], Username = usernames[lookup % usernames.Length],

View File

@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -24,10 +23,10 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
public partial class TestSceneBeatmapMetadataDisplay : OsuTestScene public partial class TestSceneBeatmapMetadataDisplay : OsuTestScene
{ {
private BeatmapMetadataDisplay display; private BeatmapMetadataDisplay display = null!;
[Resolved] [Resolved]
private BeatmapManager manager { get; set; } private BeatmapManager manager { get; set; } = null!;
[Cached(typeof(BeatmapDifficultyCache))] [Cached(typeof(BeatmapDifficultyCache))]
private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache(); private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache();
@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
{ {
private TaskCompletionSource<bool> calculationBlocker; private TaskCompletionSource<bool>? calculationBlocker;
private bool blockCalculation; private bool blockCalculation;
@ -142,10 +141,13 @@ namespace osu.Game.Tests.Visual.SongSelect
} }
} }
public override async Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo rulesetInfo = null, IEnumerable<Mod> mods = null, CancellationToken cancellationToken = default) public override async Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable<Mod>? mods = null, CancellationToken cancellationToken = default)
{ {
if (blockCalculation) if (blockCalculation)
{
Debug.Assert(calculationBlocker != null);
await calculationBlocker.Task.ConfigureAwait(false); await calculationBlocker.Task.ConfigureAwait(false);
}
return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false); return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false);
} }

View File

@ -51,7 +51,7 @@ namespace osu.Game.Tests
protected override IBeatmap GetBeatmap() => beatmap; protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => null; public override Texture GetBackground() => null;
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));

View File

@ -0,0 +1,95 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using 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<TextureUpload>
{
// The aspect ratio of SetPanelBackground at its maximum size (very tall window).
private const float minimum_display_ratio = 512 / 80f;
private readonly IResourceStore<TextureUpload>? textureStore;
public BeatmapPanelBackgroundTextureLoaderStore(IResourceStore<TextureUpload>? 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<TextureUpload> 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<string> GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty<string>();
}
}

View File

@ -23,8 +23,9 @@ namespace osu.Game.Beatmaps.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
if (working.Background != null) var background = working.GetBackground();
Texture = working.Background; if (background != null)
Texture = background;
} }
} }
} }

View File

@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => new Beatmap(); 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(); protected override Track GetBeatmapTrack() => GetVirtualTrack();

View File

@ -43,7 +43,7 @@ namespace osu.Game.Beatmaps
} }
protected override IBeatmap GetBeatmap() => beatmap; 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 override Track GetBeatmapTrack() => throw new NotImplementedException();
protected internal override ISkin GetSkin() => throw new NotImplementedException(); protected internal override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException(); public override Stream GetStream(string storagePath) => throw new NotImplementedException();

View File

@ -9,13 +9,18 @@ using osu.Game.IO;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
public interface IBeatmapResourceProvider : IStorageResourceProvider internal interface IBeatmapResourceProvider : IStorageResourceProvider
{ {
/// <summary> /// <summary>
/// Retrieve a global large texture store, used for loading beatmap backgrounds. /// Retrieve a global large texture store, used for loading beatmap backgrounds.
/// </summary> /// </summary>
TextureStore LargeTextureStore { get; } TextureStore LargeTextureStore { get; }
/// <summary>
/// Retrieve a global large texture store, used specifically for retrieving cropped beatmap panel backgrounds.
/// </summary>
TextureStore BeatmapPanelTextureStore { get; }
/// <summary> /// <summary>
/// Access a global track store for retrieving beatmap tracks from. /// Access a global track store for retrieving beatmap tracks from.
/// </summary> /// </summary>

View File

@ -32,12 +32,12 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Whether the Beatmap has finished loading. /// Whether the Beatmap has finished loading.
///</summary> ///</summary>
public bool BeatmapLoaded { get; } bool BeatmapLoaded { get; }
/// <summary> /// <summary>
/// Whether the Track has finished loading. /// Whether the Track has finished loading.
///</summary> ///</summary>
public bool TrackLoaded { get; } bool TrackLoaded { get; }
/// <summary> /// <summary>
/// Retrieves the <see cref="IBeatmap"/> which this <see cref="IWorkingBeatmap"/> represents. /// Retrieves the <see cref="IBeatmap"/> which this <see cref="IWorkingBeatmap"/> represents.
@ -47,7 +47,12 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Retrieves the background for this <see cref="IWorkingBeatmap"/>. /// Retrieves the background for this <see cref="IWorkingBeatmap"/>.
/// </summary> /// </summary>
Texture Background { get; } Texture GetBackground();
/// <summary>
/// Retrieves a cropped background for this <see cref="IWorkingBeatmap"/> used for display on panels.
/// </summary>
Texture GetPanelBackground();
/// <summary> /// <summary>
/// Retrieves the <see cref="Waveform"/> for the <see cref="Track"/> of this <see cref="IWorkingBeatmap"/>. /// Retrieves the <see cref="Waveform"/> for the <see cref="Track"/> of this <see cref="IWorkingBeatmap"/>.
@ -124,12 +129,12 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Beings loading the contents of this <see cref="IWorkingBeatmap"/> asynchronously. /// Beings loading the contents of this <see cref="IWorkingBeatmap"/> asynchronously.
/// </summary> /// </summary>
public void BeginAsyncLoad(); void BeginAsyncLoad();
/// <summary> /// <summary>
/// Cancels the asynchronous loading of the contents of this <see cref="IWorkingBeatmap"/>. /// Cancels the asynchronous loading of the contents of this <see cref="IWorkingBeatmap"/>.
/// </summary> /// </summary>
public void CancelAsyncLoad(); void CancelAsyncLoad();
/// <summary> /// <summary>
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled. /// Reads the correct track restart point from beatmap metadata and sets looping to enabled.

View File

@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
public Storyboard Storyboard => storyboard.Value; 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; public ISkin Skin => skin.Value;
private AudioManager audioManager { get; } private AudioManager audioManager { get; }
@ -67,7 +65,8 @@ namespace osu.Game.Beatmaps
protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo }; protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo };
protected abstract IBeatmap GetBeatmap(); protected abstract IBeatmap GetBeatmap();
protected abstract Texture GetBackground(); public abstract Texture GetBackground();
public virtual Texture GetPanelBackground() => GetBackground();
protected abstract Track GetBeatmapTrack(); protected abstract Track GetBeatmapTrack();
/// <summary> /// <summary>

View File

@ -42,6 +42,7 @@ namespace osu.Game.Beatmaps
private readonly AudioManager audioManager; private readonly AudioManager audioManager;
private readonly IResourceStore<byte[]> resources; private readonly IResourceStore<byte[]> resources;
private readonly LargeTextureStore largeTextureStore; private readonly LargeTextureStore largeTextureStore;
private readonly LargeTextureStore beatmapPanelTextureStore;
private readonly ITrackStore trackStore; private readonly ITrackStore trackStore;
private readonly IResourceStore<byte[]> files; private readonly IResourceStore<byte[]> files;
@ -58,6 +59,7 @@ namespace osu.Game.Beatmaps
this.host = host; this.host = host;
this.files = files; this.files = files;
largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(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; this.trackStore = trackStore;
} }
@ -110,6 +112,7 @@ namespace osu.Game.Beatmaps
#region IResourceStorageProvider #region IResourceStorageProvider
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
TextureStore IBeatmapResourceProvider.BeatmapPanelTextureStore => beatmapPanelTextureStore;
ITrackStore IBeatmapResourceProvider.Tracks => trackStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer(); IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
AudioManager IStorageResourceProvider.AudioManager => audioManager; 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)) if (string.IsNullOrEmpty(Metadata?.BackgroundFile))
return null; return null;
@ -168,7 +175,7 @@ namespace osu.Game.Beatmaps
try try
{ {
string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile); string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile);
var texture = resources.LargeTextureStore.Get(fileStorePath); var texture = store.Get(fileStorePath);
if (texture == null) if (texture == null)
{ {

View File

@ -6,6 +6,7 @@
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
@ -21,6 +22,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LowBatteryNotificationShownOnce, false); SetDefault(Static.LowBatteryNotificationShownOnce, false);
SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false); SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null);
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null); SetDefault<APISeasonalBackgrounds>(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 <see cref="SettingsOverlay"/>. /// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like <see cref="SettingsOverlay"/>.
/// </summary> /// </summary>
LastHoverSoundPlaybackTime, LastHoverSoundPlaybackTime,
/// <summary>
/// The last playback time in milliseconds of an on/off sample (from <see cref="ModSelectPanel"/>).
/// Used to debounce <see cref="ModSelectPanel"/> on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods.
/// </summary>
LastModSelectPanelSamplePlaybackTime
} }
} }

View File

@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -21,8 +18,7 @@ namespace osu.Game.Database
/// <param name="beatmapId">The beatmap to lookup.</param> /// <param name="beatmapId">The beatmap to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.</returns> /// <returns>The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull] public Task<APIBeatmap?> GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
public Task<APIBeatmap> GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
/// <summary> /// <summary>
/// Perform an API lookup on the specified beatmaps, populating a <see cref="APIBeatmap"/> model. /// Perform an API lookup on the specified beatmaps, populating a <see cref="APIBeatmap"/> model.
@ -30,10 +26,10 @@ namespace osu.Game.Database
/// <param name="beatmapIds">The beatmaps to lookup.</param> /// <param name="beatmapIds">The beatmaps to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated beatmaps. May include null results for failed retrievals.</returns> /// <returns>The populated beatmaps. May include null results for failed retrievals.</returns>
public Task<APIBeatmap[]> GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token); public Task<APIBeatmap?[]> GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
protected override GetBeatmapsRequest CreateRequest(IEnumerable<int> ids) => new GetBeatmapsRequest(ids.ToArray()); protected override GetBeatmapsRequest CreateRequest(IEnumerable<int> ids) => new GetBeatmapsRequest(ids.ToArray());
protected override IEnumerable<APIBeatmap> RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps; protected override IEnumerable<APIBeatmap>? RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
} }
} }

View File

@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Statistics; using osu.Framework.Statistics;
@ -19,8 +17,9 @@ namespace osu.Game.Database
/// Currently not persisted between game sessions. /// Currently not persisted between game sessions.
/// </summary> /// </summary>
public abstract partial class MemoryCachingComponent<TLookup, TValue> : Component public abstract partial class MemoryCachingComponent<TLookup, TValue> : Component
where TLookup : notnull
{ {
private readonly ConcurrentDictionary<TLookup, TValue> cache = new ConcurrentDictionary<TLookup, TValue>(); private readonly ConcurrentDictionary<TLookup, TValue?> cache = new ConcurrentDictionary<TLookup, TValue?>();
private readonly GlobalStatistic<MemoryCachingStatistics> statistics; private readonly GlobalStatistic<MemoryCachingStatistics> statistics;
@ -37,12 +36,12 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <param name="lookup">The lookup to retrieve.</param> /// <param name="lookup">The lookup to retrieve.</param>
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param> /// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
protected async Task<TValue> GetAsync([NotNull] TLookup lookup, CancellationToken token = default) protected async Task<TValue?> GetAsync(TLookup lookup, CancellationToken token = default)
{ {
if (CheckExists(lookup, out TValue performance)) if (CheckExists(lookup, out TValue? existing))
{ {
statistics.Value.HitCount++; statistics.Value.HitCount++;
return performance; return existing;
} }
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false); var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
@ -73,7 +72,7 @@ namespace osu.Game.Database
statistics.Value.Usage = cache.Count; 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); cache.TryGetValue(lookup, out value);
/// <summary> /// <summary>
@ -82,7 +81,7 @@ namespace osu.Game.Database
/// <param name="lookup">The lookup to retrieve.</param> /// <param name="lookup">The lookup to retrieve.</param>
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param> /// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
/// <returns>The computed value.</returns> /// <returns>The computed value.</returns>
protected abstract Task<TValue> ComputeValueAsync(TLookup lookup, CancellationToken token = default); protected abstract Task<TValue?> ComputeValueAsync(TLookup lookup, CancellationToken token = default);
private class MemoryCachingStatistics private class MemoryCachingStatistics
{ {

View File

@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -21,7 +18,7 @@ namespace osu.Game.Database
where TRequest : APIRequest where TRequest : APIRequest
{ {
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; } = null!;
/// <summary> /// <summary>
/// Creates an <see cref="APIRequest"/> to retrieve the values for a given collection of <typeparamref name="TLookup"/>s. /// Creates an <see cref="APIRequest"/> to retrieve the values for a given collection of <typeparamref name="TLookup"/>s.
@ -32,8 +29,7 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// Retrieves a list of <typeparamref name="TValue"/>s from a successful <typeparamref name="TRequest"/> created by <see cref="CreateRequest"/>. /// Retrieves a list of <typeparamref name="TValue"/>s from a successful <typeparamref name="TRequest"/> created by <see cref="CreateRequest"/>.
/// </summary> /// </summary>
[CanBeNull] protected abstract IEnumerable<TValue>? RetrieveResults(TRequest request);
protected abstract IEnumerable<TValue> RetrieveResults(TRequest request);
/// <summary> /// <summary>
/// Perform a lookup using the specified <paramref name="id"/>, populating a <typeparamref name="TValue"/>. /// Perform a lookup using the specified <paramref name="id"/>, populating a <typeparamref name="TValue"/>.
@ -41,8 +37,7 @@ namespace osu.Game.Database
/// <param name="id">The ID to lookup.</param> /// <param name="id">The ID to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated <typeparamref name="TValue"/>, or null if the value does not exist or the request could not be satisfied.</returns> /// <returns>The populated <typeparamref name="TValue"/>, or null if the value does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull] protected Task<TValue?> LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
protected Task<TValue> LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
/// <summary> /// <summary>
/// Perform an API lookup on the specified <paramref name="ids"/>, populating a <typeparamref name="TValue"/>. /// Perform an API lookup on the specified <paramref name="ids"/>, populating a <typeparamref name="TValue"/>.
@ -50,9 +45,9 @@ namespace osu.Game.Database
/// <param name="ids">The IDs to lookup.</param> /// <param name="ids">The IDs to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated values. May include null results for failed retrievals.</returns> /// <returns>The populated values. May include null results for failed retrievals.</returns>
protected Task<TValue[]> LookupAsync(TLookup[] ids, CancellationToken token = default) protected Task<TValue?[]> LookupAsync(TLookup[] ids, CancellationToken token = default)
{ {
var lookupTasks = new List<Task<TValue>>(); var lookupTasks = new List<Task<TValue?>>();
foreach (var id in ids) foreach (var id in ids)
{ {
@ -69,18 +64,18 @@ namespace osu.Game.Database
} }
// cannot be sealed due to test usages (see TestUserLookupCache). // cannot be sealed due to test usages (see TestUserLookupCache).
protected override async Task<TValue> ComputeValueAsync(TLookup lookup, CancellationToken token = default) protected override async Task<TValue?> ComputeValueAsync(TLookup lookup, CancellationToken token = default)
=> await queryValue(lookup).ConfigureAwait(false); => await queryValue(lookup).ConfigureAwait(false);
private readonly Queue<(TLookup id, TaskCompletionSource<TValue>)> pendingTasks = new Queue<(TLookup, TaskCompletionSource<TValue>)>(); private readonly Queue<(TLookup id, TaskCompletionSource<TValue?>)> pendingTasks = new Queue<(TLookup, TaskCompletionSource<TValue?>)>();
private Task pendingRequestTask; private Task? pendingRequestTask;
private readonly object taskAssignmentLock = new object(); private readonly object taskAssignmentLock = new object();
private Task<TValue> queryValue(TLookup id) private Task<TValue?> queryValue(TLookup id)
{ {
lock (taskAssignmentLock) lock (taskAssignmentLock)
{ {
var tcs = new TaskCompletionSource<TValue>(); var tcs = new TaskCompletionSource<TValue?>();
// Add to the queue. // Add to the queue.
pendingTasks.Enqueue((id, tcs)); pendingTasks.Enqueue((id, tcs));
@ -96,14 +91,14 @@ namespace osu.Game.Database
private async Task performLookup() private async Task performLookup()
{ {
// contains at most 50 unique IDs from tasks, which is used to perform the lookup. // contains at most 50 unique IDs from tasks, which is used to perform the lookup.
var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue>>>(); var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue?>>>();
// Grab at most 50 unique IDs from the queue. // Grab at most 50 unique IDs from the queue.
lock (taskAssignmentLock) lock (taskAssignmentLock)
{ {
while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50) while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50)
{ {
(TLookup id, TaskCompletionSource<TValue> task) next = pendingTasks.Dequeue(); (TLookup id, TaskCompletionSource<TValue?> task) next = pendingTasks.Dequeue();
// Perform a secondary check for existence, in case the value was queried in a previous batch. // Perform a secondary check for existence, in case the value was queried in a previous batch.
if (CheckExists(next.id, out var existing)) if (CheckExists(next.id, out var existing))
@ -113,7 +108,7 @@ namespace osu.Game.Database
if (nextTaskBatch.TryGetValue(next.id, out var tasks)) if (nextTaskBatch.TryGetValue(next.id, out var tasks))
tasks.Add(next.task); tasks.Add(next.task);
else else
nextTaskBatch[next.id] = new List<TaskCompletionSource<TValue>> { next.task }; nextTaskBatch[next.id] = new List<TaskCompletionSource<TValue?>> { next.task };
} }
} }
} }

View File

@ -22,12 +22,15 @@ using osu.Framework.Statistics;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.IO.Legacy;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Skinning; using osu.Game.Skinning;
using Realms; using Realms;
using Realms.Exceptions; using Realms.Exceptions;
@ -72,8 +75,9 @@ namespace osu.Game.Database
/// 25 2022-09-18 Remove skins to add with new naming. /// 25 2022-09-18 Remove skins to add with new naming.
/// 26 2023-02-05 Added BeatmapHash to ScoreInfo. /// 26 2023-02-05 Added BeatmapHash to ScoreInfo.
/// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo. /// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo.
/// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files.
/// </summary> /// </summary>
private const int schema_version = 27; private const int schema_version = 28;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -880,6 +884,7 @@ namespace osu.Game.Database
break; break;
case 26: case 26:
{
// Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap. // Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap.
var scores = migration.NewRealm.All<ScoreInfo>(); var scores = migration.NewRealm.All<ScoreInfo>();
@ -888,6 +893,44 @@ namespace osu.Game.Database
break; break;
} }
case 28:
{
var files = new RealmFileStore(this, storage);
var scores = migration.NewRealm.All<ScoreInfo>();
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;
}
}
} }
private string? getRulesetShortNameFromLegacyID(long rulesetId) private string? getRulesetShortNameFromLegacyID(long rulesetId)

View File

@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -21,8 +18,7 @@ namespace osu.Game.Database
/// <param name="userId">The user to lookup.</param> /// <param name="userId">The user to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated user, or null if the user does not exist or the request could not be satisfied.</returns> /// <returns>The populated user, or null if the user does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull] public Task<APIUser?> GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
public Task<APIUser> GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
/// <summary> /// <summary>
/// Perform an API lookup on the specified users, populating a <see cref="APIUser"/> model. /// Perform an API lookup on the specified users, populating a <see cref="APIUser"/> model.
@ -30,10 +26,10 @@ namespace osu.Game.Database
/// <param name="userIds">The users to lookup.</param> /// <param name="userIds">The users to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated users. May include null results for failed retrievals.</returns> /// <returns>The populated users. May include null results for failed retrievals.</returns>
public Task<APIUser[]> GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token); public Task<APIUser?[]> GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
protected override GetUsersRequest CreateRequest(IEnumerable<int> ids) => new GetUsersRequest(ids.ToArray()); protected override GetUsersRequest CreateRequest(IEnumerable<int> ids) => new GetUsersRequest(ids.ToArray());
protected override IEnumerable<APIUser> RetrieveResults(GetUsersRequest request) => request.Response?.Users; protected override IEnumerable<APIUser>? RetrieveResults(GetUsersRequest request) => request.Response?.Users;
} }
} }

View File

@ -24,7 +24,7 @@ namespace osu.Game.Graphics.Backgrounds
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(LargeTextureStore textures) 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) public override bool Equals(Background other)

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.API.Requests

View File

@ -67,7 +67,7 @@ namespace osu.Game.Online.Rooms
{ {
var beatmap = task.GetResultSafely(); var beatmap = task.GetResultSafely();
if (SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID) if (beatmap != null && SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID)
{ {
selectedBeatmap = beatmap; selectedBeatmap = beatmap;
beginTracking(); beginTracking();

View File

@ -176,7 +176,7 @@ namespace osu.Game.Overlays.Mods
dequeuedAction(); dequeuedAction();
// each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements). // 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; lastSelection = Time.Current;
} }
else else

View File

@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -45,6 +46,8 @@ namespace osu.Game.Overlays.Mods
public const float CORNER_RADIUS = 7; public const float CORNER_RADIUS = 7;
public const float HEIGHT = 42; public const float HEIGHT = 42;
public const double SAMPLE_PLAYBACK_DELAY = 30;
protected virtual float IdleSwitchWidth => 14; protected virtual float IdleSwitchWidth => 14;
protected virtual float ExpandedSwitchWidth => 30; protected virtual float ExpandedSwitchWidth => 30;
protected virtual Colour4 BackgroundColour => Active.Value ? AccentColour.Darken(0.3f) : ColourProvider.Background3; 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? sampleOff;
private Sample? sampleOn; private Sample? sampleOn;
private Bindable<double?> lastPlaybackTime = null!;
protected ModSelectPanel() protected ModSelectPanel()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -163,13 +168,15 @@ namespace osu.Game.Overlays.Mods
protected abstract void Deselect(); protected abstract void Deselect();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler) private void load(AudioManager audio, SessionStatics statics, ISamplePlaybackDisabler? samplePlaybackDisabler)
{ {
sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off"); sampleOff = audio.Samples.Get(@"UI/check-off");
if (samplePlaybackDisabler != null) if (samplePlaybackDisabler != null)
((IBindable<bool>)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); ((IBindable<bool>)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
lastPlaybackTime = statics.GetBindable<double?>(Static.LastHoverSoundPlaybackTime);
} }
protected sealed override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); protected sealed override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
@ -192,10 +199,17 @@ namespace osu.Game.Overlays.Mods
if (samplePlaybackDisabled.Value) if (samplePlaybackDisabled.Value)
return; return;
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY;
if (enoughTimePassedSinceLastPlayback)
{
if (Active.Value) if (Active.Value)
sampleOn?.Play(); sampleOn?.Play();
else else
sampleOff?.Play(); sampleOff?.Play();
lastPlaybackTime.Value = Time.Current;
}
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)

View File

@ -415,7 +415,7 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(LargeTextureStore textures) private void load(LargeTextureStore textures)
{ {
sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4"); sprite.Texture = beatmap?.GetBackground() ?? textures.Get(@"Backgrounds/bg4");
} }
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Edit.Checks
if (backgroundFile == null) if (backgroundFile == null)
yield break; yield break;
var texture = context.WorkingBeatmap.Background; var texture = context.WorkingBeatmap.GetBackground();
if (texture == null) if (texture == null)
yield break; yield break;

View File

@ -125,6 +125,7 @@ namespace osu.Game.Rulesets.Edit
if (currentSnap > DistanceSpacingMultiplier.MinValue) if (currentSnap > DistanceSpacingMultiplier.MinValue)
{ {
currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value
&& !DistanceSpacingMultiplier.Disabled
&& !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2); && !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2);
currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x"; currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x";
currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)"; currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)";
@ -141,8 +142,12 @@ namespace osu.Game.Rulesets.Edit
{ {
base.LoadComplete(); base.LoadComplete();
if (!DistanceSpacingMultiplier.Disabled) if (DistanceSpacingMultiplier.Disabled)
{ {
distanceSpacingSlider.Hide();
return;
}
DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing; DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing;
DistanceSpacingMultiplier.BindValueChanged(multiplier => DistanceSpacingMultiplier.BindValueChanged(multiplier =>
{ {
@ -163,7 +168,6 @@ namespace osu.Game.Rulesets.Edit
}); });
DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue); DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
} }
}
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{ {

View File

@ -46,6 +46,9 @@ namespace osu.Game.Scoring.Legacy
score.ScoreInfo = scoreInfo; score.ScoreInfo = scoreInfo;
int version = sr.ReadInt32(); int version = sr.ReadInt32();
scoreInfo.IsLegacyScore = version < LegacyScoreEncoder.FIRST_LAZER_VERSION;
string beatmapHash = sr.ReadString(); string beatmapHash = sr.ReadString();
workingBeatmap = GetBeatmap(beatmapHash); workingBeatmap = GetBeatmap(beatmapHash);

View File

@ -28,9 +28,10 @@ namespace osu.Game.Scoring.Legacy
/// <remarks> /// <remarks>
/// <list type="bullet"> /// <list type="bullet">
/// <item><description>30000001: Appends <see cref="LegacyReplaySoloScoreInfo"/> to the end of scores.</description></item> /// <item><description>30000001: Appends <see cref="LegacyReplaySoloScoreInfo"/> to the end of scores.</description></item>
/// <item><description>30000002: Score stored to replay calculated using the Score V2 algorithm.</description></item>
/// </list> /// </list>
/// </remarks> /// </remarks>
public const int LATEST_VERSION = 30000001; public const int LATEST_VERSION = 30000002;
/// <summary> /// <summary>
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.

View File

@ -16,7 +16,13 @@ namespace osu.Game.Scoring.Legacy
=> getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics); => getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics);
public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode) 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<HitResult, int> maximumStatistics) private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary<HitResult, int> maximumStatistics)
{ {

View File

@ -146,16 +146,48 @@ namespace osu.Game.Scoring
#pragma warning restore CS0618 #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<string, APIUser> usernameLookupCache = new Dictionary<string, APIUser>();
protected override void PostImport(ScoreInfo model, Realm realm, ImportParameters parameters) protected override void PostImport(ScoreInfo model, Realm realm, ImportParameters parameters)
{ {
base.PostImport(model, realm, parameters); base.PostImport(model, realm, parameters);
var userRequest = new GetUserRequest(model.RealmUser.Username); populateUserDetails(model);
}
/// <summary>
/// Legacy replays only store a username.
/// This will populate a user ID during import.
/// </summary>
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); api.Perform(userRequest);
if (userRequest.Response is APIUser user) 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; model.User = user;
} }
} }
} }
}

View File

@ -181,8 +181,7 @@ namespace osu.Game.Scoring
/// <summary> /// <summary>
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score. /// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
/// </summary> /// </summary>
[Ignored] public bool IsLegacyScore { get; set; }
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
private Dictionary<HitResult, int>? statistics; private Dictionary<HitResult, int>? statistics;

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
@ -21,7 +18,7 @@ namespace osu.Game.Scoring
public partial class ScorePerformanceCache : MemoryCachingComponent<ScorePerformanceCache.PerformanceCacheLookup, PerformanceAttributes> public partial class ScorePerformanceCache : MemoryCachingComponent<ScorePerformanceCache.PerformanceCacheLookup, PerformanceAttributes>
{ {
[Resolved] [Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; } private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
protected override bool CacheNullValues => false; protected override bool CacheNullValues => false;
@ -30,10 +27,10 @@ namespace osu.Game.Scoring
/// </summary> /// </summary>
/// <param name="score">The score to do the calculation on. </param> /// <param name="score">The score to do the calculation on. </param>
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param> /// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
public Task<PerformanceAttributes> CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) => public Task<PerformanceAttributes?> CalculatePerformanceAsync(ScoreInfo score, CancellationToken token = default) =>
GetAsync(new PerformanceCacheLookup(score), token); GetAsync(new PerformanceCacheLookup(score), token);
protected override async Task<PerformanceAttributes> ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default) protected override async Task<PerformanceAttributes?> ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default)
{ {
var score = lookup.ScoreInfo; var score = lookup.ScoreInfo;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -33,6 +31,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
public partial class BeatDivisorControl : CompositeDrawable, IKeyBindingHandler<GlobalAction> public partial class BeatDivisorControl : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{ {
private int? lastCustomDivisor;
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
public BeatDivisorControl(BindableBeatDivisor beatDivisor) 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) private void cycleDivisorType(int direction)
{ {
Debug.Assert(Math.Abs(direction) == 1); int totalTypes = Enum.GetValues<BeatDivisorType>().Length;
int nextDivisorType = (int)beatDivisor.ValidDivisors.Value.Type + direction; BeatDivisorType currentType = beatDivisor.ValidDivisors.Value.Type;
if (nextDivisorType > (int)BeatDivisorType.Triplets)
nextDivisorType = (int)BeatDivisorType.Common;
else if (nextDivisorType < (int)BeatDivisorType.Common)
nextDivisorType = (int)BeatDivisorType.Triplets;
switch ((BeatDivisorType)nextDivisorType) Debug.Assert(Math.Abs(direction) == 1);
cycleOnce();
if (lastCustomDivisor == null && currentType == BeatDivisorType.Custom)
cycleOnce();
switch (currentType)
{ {
case BeatDivisorType.Common: case BeatDivisorType.Common:
beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.COMMON; beatDivisor.SetArbitraryDivisor(4);
break; break;
case BeatDivisorType.Triplets: case BeatDivisorType.Triplets:
beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS; beatDivisor.SetArbitraryDivisor(6);
break; break;
case BeatDivisorType.Custom: case BeatDivisorType.Custom:
beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.Custom(beatDivisor.ValidDivisors.Value.Presets.Max()); Debug.Assert(lastCustomDivisor != null);
beatDivisor.SetArbitraryDivisor(lastCustomDivisor.Value);
break; break;
} }
void cycleOnce() => currentType = (BeatDivisorType)(((int)currentType + totalTypes + direction) % totalTypes);
} }
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
@ -326,12 +343,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
base.LoadComplete(); base.LoadComplete();
BeatDivisor.BindValueChanged(_ => updateState(), true); BeatDivisor.BindValueChanged(_ => updateState(), true);
divisorTextBox.OnCommit += (_, _) => setPresets(); divisorTextBox.OnCommit += (_, _) => setPresetsFromTextBoxEntry();
Schedule(() => GetContainingInputManager().ChangeFocus(divisorTextBox)); Schedule(() => GetContainingInputManager().ChangeFocus(divisorTextBox));
} }
private void setPresets() private void setPresetsFromTextBoxEntry()
{ {
if (!int.TryParse(divisorTextBox.Text, out int divisor) || divisor < 1 || divisor > 64) 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<int> private partial class TickSliderBar : SliderBar<int>
{ {
private Marker marker; private Marker marker = null!;
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; } = null!;
private readonly BindableBeatDivisor beatDivisor; private readonly BindableBeatDivisor beatDivisor;
@ -539,7 +556,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private partial class Marker : CompositeDrawable private partial class Marker : CompositeDrawable
{ {
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()

View File

@ -82,7 +82,7 @@ namespace osu.Game.Screens.Edit
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 5 }, Padding = new MarginPadding { Right = 5 },
}, },
new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both } new BeatDivisorControl(this.beatDivisor) { RelativeSizeAxes = Axes.Both }
}, },
}, },
RowDimensions = new[] RowDimensions = new[]

View File

@ -204,7 +204,7 @@ namespace osu.Game.Screens.Edit
protected override IBeatmap GetBeatmap() => beatmap; 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 override Track GetBeatmapTrack() => throw new NotImplementedException();

View File

@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
if (Beatmap?.BeatmapSet is IBeatmapSetOnlineInfo online) if (Beatmap?.BeatmapSet is IBeatmapSetOnlineInfo online)
texture = textures.Get(online.Covers.Cover); texture = textures.Get(online.Covers.Cover);
Sprite.Texture = texture ?? beatmaps.DefaultBeatmap.Background; Sprite.Texture = texture ?? beatmaps.DefaultBeatmap.GetBackground();
} }
public override bool Equals(Background? other) public override bool Equals(Background? other)

View File

@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play
new Sprite new Sprite
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Texture = beatmap.Background, Texture = beatmap.GetBackground(),
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
FillMode = FillMode.Fill, FillMode = FillMode.Fill,

View File

@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.HUD
protected override IBeatmap GetBeatmap() => gameplayBeatmap; 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(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();

View File

@ -42,7 +42,8 @@ namespace osu.Game.Screens.Play.HUD
//CollectionSettings = new CollectionSettings(), //CollectionSettings = new CollectionSettings(),
//DiscussionSettings = new DiscussionSettings(), //DiscussionSettings = new DiscussionSettings(),
PlaybackSettings = new PlaybackSettings { Expanded = { Value = false } }, PlaybackSettings = new PlaybackSettings { Expanded = { Value = false } },
VisualSettings = new VisualSettings { Expanded = { Value = false } } VisualSettings = new VisualSettings { Expanded = { Value = false } },
new AudioSettings { Expanded = { Value = false } }
} }
}; };
} }

View File

@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // 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;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -21,7 +23,7 @@ namespace osu.Game.Screens.Select.Carousel
Children = new Drawable[] Children = new Drawable[]
{ {
new BeatmapBackgroundSprite(working) new PanelBeatmapBackground(working)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, 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();
}
}
} }
} }

View File

@ -232,7 +232,7 @@ namespace osu.Game.Tests.Beatmaps
protected override IBeatmap GetBeatmap() => beatmap; 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 override Track GetBeatmapTrack() => throw new NotImplementedException();

View File

@ -39,7 +39,7 @@ namespace osu.Game.Tests.Beatmaps
public override Stream? GetStream(string storagePath) => null; public override Stream? GetStream(string storagePath) => null;
protected override Texture? GetBackground() => null; public override Texture? GetBackground() => null;
protected override Track? GetBeatmapTrack() => null; protected override Track? GetBeatmapTrack() => null;
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Game.Database; using osu.Game.Database;
@ -18,12 +16,12 @@ namespace osu.Game.Tests.Visual
/// </summary> /// </summary>
public const int UNRESOLVED_USER_ID = -1; public const int UNRESOLVED_USER_ID = -1;
protected override Task<APIUser> ComputeValueAsync(int lookup, CancellationToken token = default) protected override Task<APIUser?> ComputeValueAsync(int lookup, CancellationToken token = default)
{ {
if (lookup == UNRESOLVED_USER_ID) if (lookup == UNRESOLVED_USER_ID)
return Task.FromResult((APIUser)null); return Task.FromResult<APIUser?>(null);
return Task.FromResult(new APIUser return Task.FromResult<APIUser?>(new APIUser
{ {
Id = lookup, Id = lookup,
Username = $"User {lookup}" Username = $"User {lookup}"

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.20.0" /> <PackageReference Include="Realm" Version="10.20.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.531.0" /> <PackageReference Include="ppy.osu.Framework" Version="2023.608.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.510.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2023.510.0" />
<PackageReference Include="Sentry" Version="3.28.1" /> <PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -16,6 +16,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.531.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2023.608.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>