1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-16 01:03:21 +08:00

Merge branch 'master' into carousel-maintain-selection-over-update

This commit is contained in:
Dean Herbert 2022-08-16 16:04:32 +09:00
commit c8fdfd298c
120 changed files with 1494 additions and 721 deletions

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.722.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.810.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.805.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.810.2" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public class TestSceneBarLine : ManiaSkinnableTestScene
{
[Test]
public void TestMinor()
{
AddStep("Create barlines", () => recreate());
}
private void recreate(Func<IEnumerable<BarLine>>? createBarLines = null)
{
var stageDefinitions = new List<StageDefinition>
{
new StageDefinition { Columns = 4 },
};
SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s =>
{
if (createBarLines != null)
{
var barLines = createBarLines();
foreach (var b in barLines)
s.Add(b);
return;
}
for (int i = 0; i < 64; i++)
{
s.Add(new BarLine
{
StartTime = Time.Current + i * 500,
Major = i % 4 == 0,
});
}
}));
}
}
}

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => "CS"; public override string Acronym => "CS";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 0.9;
public override string Description => "No more tricky speed changes!"; public override string Description => "No more tricky speed changes!";

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 osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -16,21 +13,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
public class DrawableBarLine : DrawableManiaHitObject<BarLine> public class DrawableBarLine : DrawableManiaHitObject<BarLine>
{ {
/// <summary>
/// Height of major bar line triangles.
/// </summary>
private const float triangle_height = 12;
/// <summary>
/// Offset of the major bar line triangles from the sides of the bar line.
/// </summary>
private const float triangle_offset = 9;
public DrawableBarLine(BarLine barLine) public DrawableBarLine(BarLine barLine)
: base(barLine) : base(barLine)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 2f; Height = barLine.Major ? 1.7f : 1.2f;
AddInternal(new Box AddInternal(new Box
{ {
@ -38,34 +25,33 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = new Color4(255, 204, 33, 255), Alpha = barLine.Major ? 0.5f : 0.2f
}); });
if (barLine.Major) if (barLine.Major)
{ {
AddInternal(new EquilateralTriangle Vector2 size = new Vector2(22, 6);
const float line_offset = 4;
AddInternal(new Circle
{ {
Name = "Left triangle", Name = "Left line",
Anchor = Anchor.BottomLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.TopCentre, Origin = Anchor.CentreRight,
Size = new Vector2(triangle_height),
X = -triangle_offset, Size = size,
Rotation = 90 X = -line_offset,
}); });
AddInternal(new EquilateralTriangle AddInternal(new Circle
{ {
Name = "Right triangle", Name = "Right line",
Anchor = Anchor.BottomRight, Anchor = Anchor.CentreRight,
Origin = Anchor.TopCentre, Origin = Anchor.CentreLeft,
Size = new Vector2(triangle_height), Size = size,
X = triangle_offset, X = line_offset,
Rotation = -90
}); });
} }
if (!barLine.Major)
Alpha = 0.2f;
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
Mod = mod, Mod = mod,
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 && PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 &&
Precision.AlmostEquals(Player.GameplayClockContainer.GameplayClock.Rate, mod.SpeedChange.Value) Precision.AlmostEquals(Player.GameplayClockContainer.Rate, mod.SpeedChange.Value)
}); });
} }
} }

View File

@ -17,18 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6369583000323935d, 206, "diffcalc-test")] [TestCase(6.7115569159190587d, 206, "diffcalc-test")]
[TestCase(1.4476531024675374d, 45, "zero-length-sliders")] [TestCase(1.4391311903612753d, 45, "zero-length-sliders")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.8816128335486386d, 206, "diffcalc-test")] [TestCase(8.9757300665532966d, 206, "diffcalc-test")]
[TestCase(1.7540389962596916d, 45, "zero-length-sliders")] [TestCase(1.7437232654020756d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.6369583000323935d, 239, "diffcalc-test")] [TestCase(6.7115569159190587d, 239, "diffcalc-test")]
[TestCase(1.4476531024675374d, 54, "zero-length-sliders")] [TestCase(1.4391311903612753d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());

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.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -12,7 +10,6 @@ using osu.Framework.Audio;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -36,16 +33,16 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double spinner_duration = 6000; private const double spinner_duration = 6000;
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; } = null!;
protected override bool Autoplay => true; protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer(); protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
private DrawableSpinner drawableSpinner; private DrawableSpinner drawableSpinner = null!;
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single(); private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
[SetUpSteps] [SetUpSteps]
@ -67,12 +64,12 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
}); });
AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100)); AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100));
AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.Not.EqualTo(0).Within(100));
addSeekStep(0); addSeekStep(0);
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance)); AddAssert("is disc rotation almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(0).Within(trackerRotationTolerance));
AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(0).Within(100));
} }
[Test] [Test]
@ -100,20 +97,20 @@ namespace osu.Game.Rulesets.Osu.Tests
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in. // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
// due to the exponential damping applied we're allowing a larger margin of error of about 10% // due to the exponential damping applied we're allowing a larger margin of error of about 10%
// (5% relative to the final rotation value, but we're half-way through the spin). // (5% relative to the final rotation value, but we're half-way through the spin).
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance)); () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance));
AddAssert("symbol rotation rewound", AddAssert("symbol rotation rewound",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance)); () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation rewound", AddAssert("is cumulative rotation rewound",
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100));
addSeekStep(spinner_start_time + 5000); addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same", AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance)); () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance));
AddAssert("is symbol rotation almost same", AddAssert("is symbol rotation almost same",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance)); () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation almost same", AddAssert("is cumulative rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation, 100)); () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100));
} }
[Test] [Test]
@ -177,10 +174,10 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value); AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
addSeekStep(2000); addSeekStep(2000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); AddAssert("spm still valid", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(estimatedSpm).Within(1.0));
addSeekStep(1000); addSeekStep(1000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); AddAssert("spm still valid", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(estimatedSpm).Within(1.0));
} }
[TestCase(0.5)] [TestCase(0.5)]
@ -202,14 +199,14 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate); AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate);
addSeekStep(1000); addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); AddAssert("progress almost same", () => expectedProgress, () => Is.EqualTo(drawableSpinner.Progress).Within(0.05));
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0)); AddAssert("spm almost same", () => expectedSpm, () => Is.EqualTo(drawableSpinner.SpinsPerMinute.Value).Within(2.0));
} }
private void addSeekStep(double time) private void addSeekStep(double time)
{ {
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => time, () => Is.EqualTo(Player.DrawableRuleset.FrameStableClock.CurrentTime).Within(100));
} }
private void transformReplay(Func<Replay, Replay> replayTransformation) => AddStep("set replay", () => private void transformReplay(Func<Replay, Replay> replayTransformation) => AddStep("set replay", () =>

View File

@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
public static class AimEvaluator public static class AimEvaluator
{ {
private const double wide_angle_multiplier = 1.5; private const double wide_angle_multiplier = 1.5;
private const double acute_angle_multiplier = 2.0; private const double acute_angle_multiplier = 1.95;
private const double slider_multiplier = 1.5; private const double slider_multiplier = 1.35;
private const double velocity_change_multiplier = 0.75; private const double velocity_change_multiplier = 0.75;
/// <summary> /// <summary>
@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2); velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
} }
if (osuLastObj.TravelTime != 0) if (osuLastObj.BaseObject is Slider)
{ {
// Reward sliders based on velocity. // Reward sliders based on velocity.
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime; sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;

View File

@ -15,11 +15,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
private const double max_opacity_bonus = 0.4; private const double max_opacity_bonus = 0.4;
private const double hidden_bonus = 0.2; private const double hidden_bonus = 0.2;
private const double min_velocity = 0.5;
private const double slider_multiplier = 1.3;
/// <summary> /// <summary>
/// Evaluates the difficulty of memorising and hitting an object, based on: /// Evaluates the difficulty of memorising and hitting an object, based on:
/// <list type="bullet"> /// <list type="bullet">
/// <item><description>distance between the previous and current object,</description></item> /// <item><description>distance between a number of previous objects and the current object,</description></item>
/// <item><description>the visual opacity of the current object,</description></item> /// <item><description>the visual opacity of the current object,</description></item>
/// <item><description>length and speed of the current object (for sliders),</description></item>
/// <item><description>and whether the hidden mod is enabled.</description></item> /// <item><description>and whether the hidden mod is enabled.</description></item>
/// </list> /// </list>
/// </summary> /// </summary>
@ -73,6 +77,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (hidden) if (hidden)
result *= 1.0 + hidden_bonus; result *= 1.0 + hidden_bonus;
double sliderBonus = 0.0;
if (osuCurrent.BaseObject is Slider osuSlider)
{
// Invert the scaling factor to determine the true travel distance independent of circle size.
double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor;
// Reward sliders based on velocity.
sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5);
// Longer sliders require more memorisation.
sliderBonus *= pixelTravelDistance;
// Nerf sliders with repeats, as less memorisation is required.
if (osuSlider.RepeatCount > 0)
sliderBonus /= (osuSlider.RepeatCount + 1);
}
result += sliderBonus * slider_multiplier;
return result; return result;
} }
} }

View File

@ -46,7 +46,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
if (mods.Any(h => h is OsuModRelax)) if (mods.Any(h => h is OsuModRelax))
{
aimRating *= 0.9;
speedRating = 0.0; speedRating = 0.0;
flashlightRating *= 0.7;
}
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000; double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000; double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
@ -62,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1 Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
); );
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate; double drainRate = beatmap.Difficulty.DrainRate;

View File

@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
public class OsuPerformanceCalculator : PerformanceCalculator public class OsuPerformanceCalculator : PerformanceCalculator
{ {
public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
private double accuracy; private double accuracy;
private int scoreMaxCombo; private int scoreMaxCombo;
private int countGreat; private int countGreat;
@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
effectiveMissCount = calculateEffectiveMissCount(osuAttributes); effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. double multiplier = PERFORMANCE_BASE_MULTIPLIER;
if (score.Mods.Any(m => m is OsuModNoFail)) if (score.Mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
@ -51,10 +53,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModRelax)) if (score.Mods.Any(h => h is OsuModRelax))
{ {
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. // https://www.desmos.com/calculator/bc9eybdthb
effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits); // we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
// this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0);
double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0);
multiplier *= 0.6; // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
} }
double aimValue = computeAimValue(score, osuAttributes); double aimValue = computeAimValue(score, osuAttributes);
@ -103,7 +109,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.ApproachRate > 10.33) if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
else if (attributes.ApproachRate < 8.0) else if (attributes.ApproachRate < 8.0)
approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate); approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate);
if (score.Mods.Any(h => h is OsuModRelax))
approachRateFactor = 0.0;
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
@ -134,6 +143,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes) private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
if (score.Mods.Any(h => h is OsuModRelax))
return 0.0;
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
@ -174,7 +186,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping. // Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
return speedValue; return speedValue;
} }
@ -266,6 +278,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
} }
} }

View File

@ -15,10 +15,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{ {
public class OsuDifficultyHitObject : DifficultyHitObject public class OsuDifficultyHitObject : DifficultyHitObject
{ {
private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. /// <summary>
/// A distance by which all distances should be scaled in order to assume a uniform circle size.
/// </summary>
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25; private const int min_delta_time = 25;
private const float maximum_slider_radius = normalised_radius * 2.4f; private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
private const float assumed_slider_radius = normalised_radius * 1.8f; private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
@ -64,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public double TravelDistance { get; private set; } public double TravelDistance { get; private set; }
/// <summary> /// <summary>
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for a non-zero distance. /// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for <see cref="Slider"/> objects.
/// </summary> /// </summary>
public double TravelTime { get; private set; } public double TravelTime { get; private set; }
@ -123,7 +127,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (BaseObject is Slider currentSlider) if (BaseObject is Slider currentSlider)
{ {
computeSliderCursorPosition(currentSlider); computeSliderCursorPosition(currentSlider);
TravelDistance = currentSlider.LazyTravelDistance; // Bonus for repeat sliders until a better per nested object strain system can be achieved.
TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time); TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
} }
@ -132,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
return; return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalised_radius / (float)BaseObject.Radius; float scalingFactor = NORMALISED_RADIUS / (float)BaseObject.Radius;
if (BaseObject.Radius < 30) if (BaseObject.Radius < 30)
{ {
@ -206,7 +211,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived. slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
var currCursorPosition = slider.StackedPosition; var currCursorPosition = slider.StackedPosition;
double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. double scalingFactor = NORMALISED_RADIUS / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
for (int i = 1; i < slider.NestedHitObjects.Count; i++) for (int i = 1; i < slider.NestedHitObjects.Count; i++)
{ {
@ -234,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
else if (currMovementObj is SliderRepeat) else if (currMovementObj is SliderRepeat)
{ {
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders. // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
requiredMovement = normalised_radius; requiredMovement = NORMALISED_RADIUS;
} }
if (currMovementLength > requiredMovement) if (currMovementLength > requiredMovement)
@ -248,8 +253,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (i == slider.NestedHitObjects.Count - 1) if (i == slider.NestedHitObjects.Count - 1)
slider.LazyEndPosition = currCursorPosition; slider.LazyEndPosition = currCursorPosition;
} }
slider.LazyTravelDistance *= (float)Math.Pow(1 + slider.RepeatCount / 2.5, 1.0 / 2.5); // Bonus for repeat sliders until a better per nested object strain system can be achieved.
} }
private Vector2 getEndCursorPosition(OsuHitObject hitObject) private Vector2 getEndCursorPosition(OsuHitObject hitObject)

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double currentStrain; private double currentStrain;
private double skillMultiplier => 23.25; private double skillMultiplier => 23.55;
private double strainDecayBase => 0.15; private double strainDecayBase => 0.15;
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => OsuIcon.ModAutopilot; public override IconUsage? Icon => OsuIcon.ModAutopilot;
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 0.1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public bool PerformFail() => false; public bool PerformFail() => false;

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.Magnet; public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circles your cursor is a magnet!"; public override string Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
private IFrameStableClock gameplayClock = null!; private IFrameStableClock gameplayClock = null!;

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private bool rotationTransferred; private bool rotationTransferred;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private GameplayClock gameplayClock { get; set; } private IGameplayClock gameplayClock { get; set; }
protected override void Update() protected override void Update()
{ {

View File

@ -206,7 +206,7 @@ namespace osu.Game.Tests.Editing
} }
private void assertSnapDistance(float expectedDistance, HitObject hitObject = null) private void assertSnapDistance(float expectedDistance, HitObject hitObject = null)
=> AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()) == expectedDistance); => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()), () => Is.EqualTo(expectedDistance));
private void assertDurationToDistance(double duration, float expectedDistance) private void assertDurationToDistance(double duration, float expectedDistance)
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance); => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance);

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Gameplay
}); });
AddStep("start clock", () => gameplayClockContainer.Start()); AddStep("start clock", () => gameplayClockContainer.Start());
AddUntilStep("elapsed greater than zero", () => gameplayClockContainer.GameplayClock.ElapsedFrameTime > 0); AddUntilStep("elapsed greater than zero", () => gameplayClockContainer.ElapsedFrameTime > 0);
} }
[Test] [Test]
@ -60,16 +60,16 @@ namespace osu.Game.Tests.Gameplay
}); });
AddStep("start clock", () => gameplayClockContainer.Start()); AddStep("start clock", () => gameplayClockContainer.Start());
AddUntilStep("current time greater 2000", () => gameplayClockContainer.GameplayClock.CurrentTime > 2000); AddUntilStep("current time greater 2000", () => gameplayClockContainer.CurrentTime > 2000);
double timeAtReset = 0; double timeAtReset = 0;
AddStep("reset clock", () => AddStep("reset clock", () =>
{ {
timeAtReset = gameplayClockContainer.GameplayClock.CurrentTime; timeAtReset = gameplayClockContainer.CurrentTime;
gameplayClockContainer.Reset(); gameplayClockContainer.Reset();
}); });
AddAssert("current time < time at reset", () => gameplayClockContainer.GameplayClock.CurrentTime < timeAtReset); AddAssert("current time < time at reset", () => gameplayClockContainer.CurrentTime < timeAtReset);
} }
[Test] [Test]

View File

@ -77,7 +77,6 @@ namespace osu.Game.Tests.Gameplay
Add(gameplayContainer = new MasterGameplayClockContainer(working, 0) Add(gameplayContainer = new MasterGameplayClockContainer(working, 0)
{ {
IsPaused = { Value = true },
Child = new FrameStabilityContainer Child = new FrameStabilityContainer
{ {
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
@ -106,7 +105,6 @@ namespace osu.Game.Tests.Gameplay
Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time) Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time)
{ {
StartTime = start_time, StartTime = start_time,
IsPaused = { Value = true },
Child = new FrameStabilityContainer Child = new FrameStabilityContainer
{ {
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
@ -141,7 +139,7 @@ namespace osu.Game.Tests.Gameplay
beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
{ {
Clock = gameplayContainer.GameplayClock Clock = gameplayContainer
}); });
}); });

View File

@ -36,6 +36,23 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single())); Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
} }
[Test]
public void TestAudioEqualityCaseSensitivity()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
// empty by default so let's set it..
beatmapSetA.Beatmaps.First().Metadata.AudioFile = "audio.mp3";
beatmapSetB.Beatmaps.First().Metadata.AudioFile = "audio.mp3";
addAudioFile(beatmapSetA, "abc", "AuDiO.mP3");
addAudioFile(beatmapSetB, "abc", "audio.mp3");
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
[Test] [Test]
public void TestAudioEqualitySameHash() public void TestAudioEqualitySameHash()
{ {

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -30,7 +31,7 @@ namespace osu.Game.Tests.NonVisual
{ {
public List<Bindable<double>> MutableNonGameplayAdjustments { get; } = new List<Bindable<double>>(); public List<Bindable<double>> MutableNonGameplayAdjustments { get; } = new List<Bindable<double>>();
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments; public override IEnumerable<double> NonGameplayAdjustments => MutableNonGameplayAdjustments.Select(b => b.Value);
public TestGameplayClock(IFrameBasedClock underlyingClock) public TestGameplayClock(IFrameBasedClock underlyingClock)
: base(underlyingClock) : base(underlyingClock)

View File

@ -128,6 +128,8 @@ namespace osu.Game.Tests.Resources
var rulesetInfo = getRuleset(); var rulesetInfo = getRuleset();
string hash = Guid.NewGuid().ToString().ComputeMD5Hash();
yield return new BeatmapInfo yield return new BeatmapInfo
{ {
OnlineID = beatmapId, OnlineID = beatmapId,
@ -136,7 +138,8 @@ namespace osu.Game.Tests.Resources
Length = length, Length = length,
BeatmapSet = beatmapSet, BeatmapSet = beatmapSet,
BPM = bpm, BPM = bpm,
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), Hash = hash,
MD5Hash = hash,
Ruleset = rulesetInfo, Ruleset = rulesetInfo,
Metadata = metadata.DeepClone(), Metadata = metadata.DeepClone(),
Difficulty = new BeatmapDifficulty Difficulty = new BeatmapDifficulty

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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -59,15 +57,15 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("reset clock", () => Clock.Seek(0)); AddStep("reset clock", () => Clock.Seek(0));
AddStep("start clock", Clock.Start); AddStep("start clock", () => Clock.Start());
AddAssert("clock running", () => Clock.IsRunning); AddAssert("clock running", () => Clock.IsRunning);
AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250)); AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250));
AddUntilStep("clock stops", () => !Clock.IsRunning); AddUntilStep("clock stops", () => !Clock.IsRunning);
AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); AddUntilStep("clock stopped at end", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength));
AddStep("start clock again", Clock.Start); AddStep("start clock again", () => Clock.Start());
AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
} }
@ -76,32 +74,32 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("reset clock", () => Clock.Seek(0)); AddStep("reset clock", () => Clock.Seek(0));
AddStep("stop clock", Clock.Stop); AddStep("stop clock", () => Clock.Stop());
AddAssert("clock stopped", () => !Clock.IsRunning); AddAssert("clock stopped", () => !Clock.IsRunning);
AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength)); AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength));
AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); AddAssert("clock stopped at end", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength));
AddStep("start clock again", Clock.Start); AddStep("start clock again", () => Clock.Start());
AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
} }
[Test] [Test]
public void TestClampWhenSeekOutsideBeatmapBounds() public void TestClampWhenSeekOutsideBeatmapBounds()
{ {
AddStep("stop clock", Clock.Stop); AddStep("stop clock", () => Clock.Stop());
AddStep("seek before start time", () => Clock.Seek(-1000)); AddStep("seek before start time", () => Clock.Seek(-1000));
AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0); AddAssert("time is clamped to 0", () => Clock.CurrentTime, () => Is.EqualTo(0));
AddStep("seek beyond track length", () => Clock.Seek(Clock.TrackLength + 1000)); AddStep("seek beyond track length", () => Clock.Seek(Clock.TrackLength + 1000));
AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength); AddAssert("time is clamped to track length", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength));
AddStep("seek smoothly before start time", () => Clock.SeekSmoothlyTo(-1000)); AddStep("seek smoothly before start time", () => Clock.SeekSmoothlyTo(-1000));
AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0); AddUntilStep("time is clamped to 0", () => Clock.CurrentTime, () => Is.EqualTo(0));
AddStep("seek smoothly beyond track length", () => Clock.SeekSmoothlyTo(Clock.TrackLength + 1000)); AddStep("seek smoothly beyond track length", () => Clock.SeekSmoothlyTo(Clock.TrackLength + 1000));
AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength); AddUntilStep("time is clamped to track length", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength));
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -60,17 +60,17 @@ namespace osu.Game.Tests.Visual.Editing
// Forwards // Forwards
AddStep("Seek(0)", () => Clock.Seek(0)); AddStep("Seek(0)", () => Clock.Seek(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
AddStep("Seek(33)", () => Clock.Seek(33)); AddStep("Seek(33)", () => Clock.Seek(33));
AddAssert("Time = 33", () => Clock.CurrentTime == 33); checkTime(33);
AddStep("Seek(89)", () => Clock.Seek(89)); AddStep("Seek(89)", () => Clock.Seek(89));
AddAssert("Time = 89", () => Clock.CurrentTime == 89); checkTime(89);
// Backwards // Backwards
AddStep("Seek(25)", () => Clock.Seek(25)); AddStep("Seek(25)", () => Clock.Seek(25));
AddAssert("Time = 25", () => Clock.CurrentTime == 25); checkTime(25);
AddStep("Seek(0)", () => Clock.Seek(0)); AddStep("Seek(0)", () => Clock.Seek(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
/// <summary> /// <summary>
@ -83,19 +83,19 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("Seek(0), Snap", () => Clock.SeekSnapped(0)); AddStep("Seek(0), Snap", () => Clock.SeekSnapped(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
AddStep("Seek(50), Snap", () => Clock.SeekSnapped(50)); AddStep("Seek(50), Snap", () => Clock.SeekSnapped(50));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("Seek(100), Snap", () => Clock.SeekSnapped(100)); AddStep("Seek(100), Snap", () => Clock.SeekSnapped(100));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(175), Snap", () => Clock.SeekSnapped(175)); AddStep("Seek(175), Snap", () => Clock.SeekSnapped(175));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(350), Snap", () => Clock.SeekSnapped(350)); AddStep("Seek(350), Snap", () => Clock.SeekSnapped(350));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("Seek(400), Snap", () => Clock.SeekSnapped(400)); AddStep("Seek(400), Snap", () => Clock.SeekSnapped(400));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("Seek(450), Snap", () => Clock.SeekSnapped(450)); AddStep("Seek(450), Snap", () => Clock.SeekSnapped(450));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -108,17 +108,17 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("Seek(24), Snap", () => Clock.SeekSnapped(24)); AddStep("Seek(24), Snap", () => Clock.SeekSnapped(24));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
AddStep("Seek(26), Snap", () => Clock.SeekSnapped(26)); AddStep("Seek(26), Snap", () => Clock.SeekSnapped(26));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("Seek(150), Snap", () => Clock.SeekSnapped(150)); AddStep("Seek(150), Snap", () => Clock.SeekSnapped(150));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(170), Snap", () => Clock.SeekSnapped(170)); AddStep("Seek(170), Snap", () => Clock.SeekSnapped(170));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(274), Snap", () => Clock.SeekSnapped(274)); AddStep("Seek(274), Snap", () => Clock.SeekSnapped(274));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(276), Snap", () => Clock.SeekSnapped(276)); AddStep("Seek(276), Snap", () => Clock.SeekSnapped(276));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
} }
/// <summary> /// <summary>
@ -130,15 +130,15 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 200", () => Clock.CurrentTime == 200); checkTime(200);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -150,17 +150,17 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -174,28 +174,28 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(49)", () => Clock.Seek(49)); AddStep("Seek(49)", () => Clock.Seek(49));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("Seek(49.999)", () => Clock.Seek(49.999)); AddStep("Seek(49.999)", () => Clock.Seek(49.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(99)", () => Clock.Seek(99)); AddStep("Seek(99)", () => Clock.Seek(99));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(99.999)", () => Clock.Seek(99.999)); AddStep("Seek(99.999)", () => Clock.Seek(99.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 150); checkTime(150);
AddStep("Seek(174)", () => Clock.Seek(174)); AddStep("Seek(174)", () => Clock.Seek(174));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(349)", () => Clock.Seek(349)); AddStep("Seek(349)", () => Clock.Seek(349));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("Seek(399)", () => Clock.Seek(399)); AddStep("Seek(399)", () => Clock.Seek(399));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("Seek(449)", () => Clock.Seek(449)); AddStep("Seek(449)", () => Clock.Seek(449));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -208,15 +208,15 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(450)", () => Clock.Seek(450)); AddStep("Seek(450)", () => Clock.Seek(450));
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 150", () => Clock.CurrentTime == 150); checkTime(150);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
/// <summary> /// <summary>
@ -229,17 +229,17 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(450)", () => Clock.Seek(450)); AddStep("Seek(450)", () => Clock.Seek(450));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
/// <summary> /// <summary>
@ -253,16 +253,16 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(451)", () => Clock.Seek(451)); AddStep("Seek(451)", () => Clock.Seek(451));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
AddStep("Seek(450.999)", () => Clock.Seek(450.999)); AddStep("Seek(450.999)", () => Clock.Seek(450.999));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
AddStep("Seek(401)", () => Clock.Seek(401)); AddStep("Seek(401)", () => Clock.Seek(401));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("Seek(401.999)", () => Clock.Seek(401.999)); AddStep("Seek(401.999)", () => Clock.Seek(401.999));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
} }
/// <summary> /// <summary>
@ -297,9 +297,11 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Time < lastTime", () => Clock.CurrentTime < lastTime); AddAssert("Time < lastTime", () => Clock.CurrentTime < lastTime);
} }
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
private void checkTime(double expectedTime) => AddAssert($"Current time is {expectedTime}", () => Clock.CurrentTime, () => Is.EqualTo(expectedTime));
private void reset() private void reset()
{ {
AddStep("Reset", () => Clock.Seek(0)); AddStep("Reset", () => Clock.Seek(0));

View File

@ -4,7 +4,6 @@
#nullable disable #nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -120,7 +119,7 @@ namespace osu.Game.Tests.Visual.Editing
private void pressAndCheckTime(Key key, double expectedTime) private void pressAndCheckTime(Key key, double expectedTime)
{ {
AddStep($"press {key}", () => InputManager.Key(key)); AddStep($"press {key}", () => InputManager.Key(key));
AddUntilStep($"time is {expectedTime}", () => Precision.AlmostEquals(expectedTime, EditorClock.CurrentTime, 1)); AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1));
} }
} }
} }

View File

@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Gameplay
(typeof(ScoreProcessor), actualComponentsContainer.Dependencies.Get<ScoreProcessor>()), (typeof(ScoreProcessor), actualComponentsContainer.Dependencies.Get<ScoreProcessor>()),
(typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get<HealthProcessor>()), (typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get<HealthProcessor>()),
(typeof(GameplayState), actualComponentsContainer.Dependencies.Get<GameplayState>()), (typeof(GameplayState), actualComponentsContainer.Dependencies.Get<GameplayState>()),
(typeof(GameplayClock), actualComponentsContainer.Dependencies.Get<GameplayClock>()) (typeof(IGameplayClock), actualComponentsContainer.Dependencies.Get<IGameplayClock>())
}, },
}; };

View File

@ -137,13 +137,13 @@ namespace osu.Game.Tests.Visual.Gameplay
private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time); private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time);
private void confirmSeek(double time) => AddUntilStep($"wait for seek to {time}", () => consumer.Clock.CurrentTime == time); private void confirmSeek(double time) => AddUntilStep($"wait for seek to {time}", () => consumer.Clock.CurrentTime, () => Is.EqualTo(time));
private void checkFrameCount(int frames) => private void checkFrameCount(int frames) =>
AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames); AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames, () => Is.EqualTo(frames));
private void checkRate(double rate) => private void checkRate(double rate) =>
AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate); AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate, () => Is.EqualTo(rate));
public class ClockConsumingChild : CompositeDrawable public class ClockConsumingChild : CompositeDrawable
{ {

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.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@ -21,22 +19,22 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestAllSamplesStopDuringSeek() public void TestAllSamplesStopDuringSeek()
{ {
DrawableSlider slider = null; DrawableSlider? slider = null;
PoolableSkinnableSample[] samples = null; PoolableSkinnableSample[] samples = null!;
ISamplePlaybackDisabler sampleDisabler = null; ISamplePlaybackDisabler sampleDisabler = null!;
AddUntilStep("get variables", () => AddUntilStep("get variables", () =>
{ {
sampleDisabler = Player; sampleDisabler = Player;
slider = Player.ChildrenOfType<DrawableSlider>().MinBy(s => s.HitObject.StartTime); slider = Player.ChildrenOfType<DrawableSlider>().MinBy(s => s.HitObject.StartTime);
samples = slider?.ChildrenOfType<PoolableSkinnableSample>().ToArray(); samples = slider.ChildrenOfType<PoolableSkinnableSample>().ToArray();
return slider != null; return slider != null;
}); });
AddUntilStep("wait for slider sliding then seek", () => AddUntilStep("wait for slider sliding then seek", () =>
{ {
if (!slider.Tracking.Value) if (slider?.Tracking.Value != true)
return false; return false;
if (!samples.Any(s => s.Playing)) if (!samples.Any(s => s.Playing))

View File

@ -38,8 +38,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached] [Cached]
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached] [Cached(typeof(IGameplayClock))]
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock());
// best way to check without exposing. // best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter; private Drawable hideTarget => hudOverlay.KeyCounter;

View File

@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public double FirstHitObjectTime => DrawableRuleset.Objects.First().StartTime; public double FirstHitObjectTime => DrawableRuleset.Objects.First().StartTime;
public double GameplayClockTime => GameplayClockContainer.GameplayClock.CurrentTime; public double GameplayClockTime => GameplayClockContainer.CurrentTime;
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Gameplay
if (!FirstFrameClockTime.HasValue) if (!FirstFrameClockTime.HasValue)
{ {
FirstFrameClockTime = GameplayClockContainer.GameplayClock.CurrentTime; FirstFrameClockTime = GameplayClockContainer.CurrentTime;
AddInternal(new OsuSpriteText AddInternal(new OsuSpriteText
{ {
Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} " Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} "

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay
base.SetUpSteps(); base.SetUpSteps();
AddUntilStep("gameplay has started", AddUntilStep("gameplay has started",
() => Player.GameplayClockContainer.GameplayClock.CurrentTime > Player.DrawableRuleset.GameplayStartTime); () => Player.GameplayClockContainer.CurrentTime > Player.DrawableRuleset.GameplayStartTime);
} }
[Test] [Test]

View File

@ -313,7 +313,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("pause again", () => AddUntilStep("pause again", () =>
{ {
Player.Pause(); Player.Pause();
return !Player.GameplayClockContainer.GameplayClock.IsRunning; return !Player.GameplayClockContainer.IsRunning;
}); });
AddAssert("loop is playing", () => getLoop().IsPlaying); AddAssert("loop is playing", () => getLoop().IsPlaying);
@ -378,7 +378,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown); AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown);
private void confirmClockRunning(bool isRunning) => private void confirmClockRunning(bool isRunning) =>
AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.GameplayClock.IsRunning == isRunning); AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.IsRunning == isRunning);
protected override bool AllowFail => true; protected override bool AllowFail => true;

View File

@ -56,6 +56,10 @@ namespace osu.Game.Tests.Visual.Gameplay
private readonly ChangelogOverlay changelogOverlay; private readonly ChangelogOverlay changelogOverlay;
private double savedTrackVolume;
private double savedMasterVolume;
private bool savedMutedState;
public TestScenePlayerLoader() public TestScenePlayerLoader()
{ {
AddRange(new Drawable[] AddRange(new Drawable[]
@ -75,11 +79,7 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() => player = null);
{
player = null;
audioManager.Volume.SetDefault();
});
/// <summary> /// <summary>
/// Sets the input manager child to a new test player loader container instance. /// Sets the input manager child to a new test player loader container instance.
@ -147,6 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay
moveMouse(); moveMouse();
return player?.LoadState == LoadState.Ready; return player?.LoadState == LoadState.Ready;
}); });
AddRepeatStep("move mouse", moveMouse, 20); AddRepeatStep("move mouse", moveMouse, 20);
AddAssert("loader still active", () => loader.IsCurrentScreen()); AddAssert("loader still active", () => loader.IsCurrentScreen());
@ -154,6 +155,8 @@ namespace osu.Game.Tests.Visual.Gameplay
void moveMouse() void moveMouse()
{ {
notificationOverlay.State.Value = Visibility.Hidden;
InputManager.MoveMouseTo( InputManager.MoveMouseTo(
loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft
+ (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft) + (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft)
@ -274,6 +277,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("load player", () => resetPlayer(false, beforeLoad)); AddStep("load player", () => resetPlayer(false, beforeLoad));
AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready);
saveVolumes();
AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1); AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1);
AddStep("click notification", () => AddStep("click notification", () =>
{ {
@ -287,6 +292,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("check " + volumeName, assert); AddAssert("check " + volumeName, assert);
restoreVolumes();
AddUntilStep("wait for player load", () => player.IsLoaded); AddUntilStep("wait for player load", () => player.IsLoaded);
} }
@ -294,6 +301,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestCase(false)] [TestCase(false)]
public void TestEpilepsyWarning(bool warning) public void TestEpilepsyWarning(bool warning)
{ {
saveVolumes();
setFullVolume();
AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("change epilepsy warning", () => epilepsyWarning = warning);
AddStep("load dummy beatmap", () => resetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
@ -306,6 +316,30 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("sound volume decreased", () => Beatmap.Value.Track.AggregateVolume.Value == 0.25); AddUntilStep("sound volume decreased", () => Beatmap.Value.Track.AggregateVolume.Value == 0.25);
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1);
} }
restoreVolumes();
}
[Test]
public void TestEpilepsyWarningEarlyExit()
{
saveVolumes();
setFullVolume();
AddStep("set epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0);
AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible);
AddStep("exit early", () => loader.Exit());
AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden);
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1);
restoreVolumes();
} }
[TestCase(true, 1.0, false)] // on battery, above cutoff --> no warning [TestCase(true, 1.0, false)] // on battery, above cutoff --> no warning
@ -336,21 +370,34 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for player load", () => player.IsLoaded); AddUntilStep("wait for player load", () => player.IsLoaded);
} }
[Test] private void restoreVolumes()
public void TestEpilepsyWarningEarlyExit()
{ {
AddStep("set epilepsy warning", () => epilepsyWarning = true); AddStep("restore previous volumes", () =>
AddStep("load dummy beatmap", () => resetPlayer(false)); {
audioManager.VolumeTrack.Value = savedTrackVolume;
audioManager.Volume.Value = savedMasterVolume;
volumeOverlay.IsMuted.Value = savedMutedState;
});
}
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); private void setFullVolume()
{
AddStep("set volumes to 100%", () =>
{
audioManager.VolumeTrack.Value = 1;
audioManager.Volume.Value = 1;
volumeOverlay.IsMuted.Value = false;
});
}
AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0); private void saveVolumes()
AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible); {
AddStep("save previous volumes", () =>
AddStep("exit early", () => loader.Exit()); {
savedTrackVolume = audioManager.VolumeTrack.Value;
AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden); savedMasterVolume = audioManager.Volume.Value;
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); savedMutedState = volumeOverlay.IsMuted.Value;
});
} }
private EpilepsyWarning getWarning() => loader.ChildrenOfType<EpilepsyWarning>().SingleOrDefault(); private EpilepsyWarning getWarning() => loader.ChildrenOfType<EpilepsyWarning>().SingleOrDefault();

View File

@ -29,8 +29,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached] [Cached]
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached] [Cached(typeof(IGameplayClock))]
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock());
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()

View File

@ -36,8 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached] [Cached]
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached] [Cached(typeof(IGameplayClock))]
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock());
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>(); private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();

View File

@ -6,6 +6,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK; using osuTK;
@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private double increment; private double increment;
private GameplayClockContainer gameplayClockContainer; private GameplayClockContainer gameplayClockContainer;
private GameplayClock gameplayClock; private IFrameBasedClock gameplayClock;
private const double skip_time = 6000; private const double skip_time = 6000;
@ -51,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}; };
gameplayClockContainer.Start(); gameplayClockContainer.Start();
gameplayClock = gameplayClockContainer.GameplayClock; gameplayClock = gameplayClockContainer;
}); });
[Test] [Test]

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)); Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time));
Dependencies.CacheAs(gameplayClockContainer.GameplayClock); Dependencies.CacheAs<IGameplayClock>(gameplayClockContainer);
} }
[SetUpSteps] [SetUpSteps]

View File

@ -363,7 +363,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private Player player => Stack.CurrentScreen as Player; private Player player => Stack.CurrentScreen as Player;
private double currentFrameStableTime private double currentFrameStableTime
=> player.ChildrenOfType<FrameStabilityContainer>().First().FrameStableClock.CurrentTime; => player.ChildrenOfType<FrameStabilityContainer>().First().CurrentTime;
private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true);

View File

@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestStoryboardNoSkipOutro() public void TestStoryboardNoSkipOutro()
{ {
CreateTest(); CreateTest();
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for score shown", () => Player.IsScoreShown); AddUntilStep("wait for score shown", () => Player.IsScoreShown);
} }
@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
} }
@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("set ShowResults = false", () => showResults = false); AddStep("set ShowResults = false", () => showResults = false);
}); });
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
AddWaitStep("wait", 10); AddWaitStep("wait", 10);
AddAssert("no score shown", () => !Player.IsScoreShown); AddAssert("no score shown", () => !Player.IsScoreShown);
} }
@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestStoryboardEndsBeforeCompletion() public void TestStoryboardEndsBeforeCompletion()
{ {
CreateTest(() => AddStep("set storyboard duration to .1s", () => currentStoryboardDuration = 100)); CreateTest(() => AddStep("set storyboard duration to .1s", () => currentStoryboardDuration = 100));
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("wait for score shown", () => Player.IsScoreShown); AddUntilStep("wait for score shown", () => Player.IsScoreShown);
} }
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("skip overlay content not visible", () => fadeContainer().State == Visibility.Hidden); AddUntilStep("skip overlay content not visible", () => fadeContainer().State == Visibility.Hidden);
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible); AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
} }
[Test] [Test]

View File

@ -0,0 +1,87 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API;
using osu.Game.Overlays.Toolbar;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
public class TestSceneToolbarUserButton : OsuManualInputManagerTestScene
{
public TestSceneToolbarUserButton()
{
Container mainContainer;
Children = new Drawable[]
{
mainContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = Toolbar.HEIGHT,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new Box
{
Colour = Color4.DarkRed,
RelativeSizeAxes = Axes.Y,
Width = 2,
},
new ToolbarUserButton(),
new Box
{
Colour = Color4.DarkRed,
RelativeSizeAxes = Axes.Y,
Width = 2,
},
}
},
}
},
};
AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale));
}
[Test]
public void TestLoginLogout()
{
AddStep("Log out", () => ((DummyAPIAccess)API).Logout());
AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
}
[Test]
public void TestStates()
{
AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
foreach (var state in Enum.GetValues<APIState>())
{
AddStep($"Change state to {state}", () => ((DummyAPIAccess)API).SetState(state));
}
}
}
}

View File

@ -432,8 +432,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
var user = playingUsers.Single(u => u.UserID == userId); var user = playingUsers.Single(u => u.UserID == userId);
OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull());
SpectatorClient.SendEndPlay(userId); SpectatorClient.SendEndPlay(userId);
OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull());
playingUsers.Remove(user); playingUsers.Remove(user);
}); });
@ -451,7 +451,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
private void checkPaused(int userId, bool state) private void checkPaused(int userId, bool state)
=> AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().GameplayClock.IsRunning != state); => AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().IsRunning != state);
private void checkPausedInstant(int userId, bool state) private void checkPausedInstant(int userId, bool state)
{ {

View File

@ -671,7 +671,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000) for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000)
{ {
double time = i; double time = i;
AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.GameplayClock.CurrentTime > time); AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.CurrentTime > time);
} }
AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);

View File

@ -134,6 +134,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestSoftDeleteSupport() public void TestSoftDeleteSupport()
{ {
AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0));
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddStep("create content", () => Child = new ModPresetColumn AddStep("create content", () => Child = new ModPresetColumn
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -153,9 +154,11 @@ namespace osu.Game.Tests.Visual.UserInterface
foreach (var preset in r.All<ModPreset>()) foreach (var preset in r.All<ModPreset>())
preset.DeletePending = true; preset.DeletePending = true;
})); }));
AddUntilStep("no panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 0); AddUntilStep("no panels visible", () => !this.ChildrenOfType<ModPresetPanel>().Any());
AddStep("undelete preset", () => Realm.Write(r => AddStep("select mods from first preset", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() });
AddStep("undelete presets", () => Realm.Write(r =>
{ {
foreach (var preset in r.All<ModPreset>()) foreach (var preset in r.All<ModPreset>())
preset.DeletePending = false; preset.DeletePending = false;

View File

@ -1,18 +1,20 @@
// 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.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Music; using osu.Game.Overlays.Music;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -21,13 +23,25 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene
{ {
private readonly BindableList<Live<BeatmapSetInfo>> beatmapSets = new BindableList<Live<BeatmapSetInfo>>(); protected override bool UseFreshStoragePerRun => true;
private PlaylistOverlay playlistOverlay; private PlaylistOverlay playlistOverlay = null!;
private Live<BeatmapSetInfo> first; private BeatmapManager beatmapManager = null!;
private const int item_count = 100; private const int item_count = 20;
private List<BeatmapSetInfo> beatmapSets => beatmapManager.GetAllUsableBeatmapSets();
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
}
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
@ -46,16 +60,12 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
}; };
beatmapSets.Clear();
for (int i = 0; i < item_count; i++) for (int i = 0; i < item_count; i++)
{ {
beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo().ToLiveUnmanaged()); beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo());
} }
first = beatmapSets.First(); beatmapSets.First().ToLive(Realm);
playlistOverlay.BeatmapSets.BindTo(beatmapSets);
}); });
[Test] [Test]
@ -70,9 +80,13 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any()); AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any());
PlaylistItem firstItem = null!;
AddStep("hold 1st item handle", () => AddStep("hold 1st item handle", () =>
{ {
var handle = this.ChildrenOfType<OsuRearrangeableListItem<Live<BeatmapSetInfo>>.PlaylistItemHandle>().First(); firstItem = this.ChildrenOfType<PlaylistItem>().First();
var handle = firstItem.ChildrenOfType<PlaylistItem.PlaylistItemHandle>().First();
InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre); InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre);
InputManager.PressButton(MouseButton.Left); InputManager.PressButton(MouseButton.Left);
}); });
@ -83,7 +97,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.BottomLeft); InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.BottomLeft);
}); });
AddAssert("song 1 is 5th", () => beatmapSets[4].Equals(first)); AddAssert("first is moved", () => playlistOverlay.ChildrenOfType<Playlist>().Single().Items.ElementAt(4).Value.Equals(firstItem.Model.Value));
AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left));
} }
@ -101,6 +115,68 @@ namespace osu.Game.Tests.Visual.UserInterface
() => playlistOverlay.ChildrenOfType<PlaylistItem>() () => playlistOverlay.ChildrenOfType<PlaylistItem>()
.Where(item => item.MatchingFilter) .Where(item => item.MatchingFilter)
.All(item => item.FilterTerms.Any(term => term.ToString().Contains("10")))); .All(item => item.FilterTerms.Any(term => term.ToString().Contains("10"))));
AddStep("Import new non-matching beatmap", () =>
{
var testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(1);
testBeatmapSetInfo.Beatmaps.Single().Metadata.Title = "no guid";
beatmapManager.Import(testBeatmapSetInfo);
});
AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh()));
AddAssert("results filtered correctly",
() => playlistOverlay.ChildrenOfType<PlaylistItem>()
.Where(item => item.MatchingFilter)
.All(item => item.FilterTerms.Any(term => term.ToString().Contains("10"))));
}
[Test]
public void TestCollectionFiltering()
{
NowPlayingCollectionDropdown collectionDropdown() => playlistOverlay.ChildrenOfType<NowPlayingCollectionDropdown>().Single();
AddStep("Add collection", () =>
{
Dependencies.Get<RealmAccess>().Write(r =>
{
r.RemoveAll<BeatmapCollection>();
r.Add(new BeatmapCollection("wang"));
});
});
AddUntilStep("wait for dropdown to have new collection", () => collectionDropdown().Items.Count() == 2);
AddStep("Filter to collection", () =>
{
collectionDropdown().Current.Value = collectionDropdown().Items.Last();
});
AddUntilStep("No items present", () => !playlistOverlay.ChildrenOfType<PlaylistItem>().Any(i => i.MatchingFilter));
AddStep("Import new non-matching beatmap", () =>
{
beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(1));
});
AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh()));
AddUntilStep("No items matching", () => !playlistOverlay.ChildrenOfType<PlaylistItem>().Any(i => i.MatchingFilter));
BeatmapSetInfo collectionAddedBeatmapSet = null!;
AddStep("Import new matching beatmap", () =>
{
collectionAddedBeatmapSet = TestResources.CreateTestBeatmapSetInfo(1);
beatmapManager.Import(collectionAddedBeatmapSet);
Realm.Write(r => r.All<BeatmapCollection>().First().BeatmapMD5Hashes.Add(collectionAddedBeatmapSet.Beatmaps.First().MD5Hash));
});
AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh()));
AddUntilStep("Only matching item",
() => playlistOverlay.ChildrenOfType<PlaylistItem>().Where(i => i.MatchingFilter).Select(i => i.Model.ID), () => Is.EquivalentTo(new[] { collectionAddedBeatmapSet.ID }));
} }
} }
} }

View File

@ -199,8 +199,8 @@ namespace osu.Game.Beatmaps
Debug.Assert(x.BeatmapSet != null); Debug.Assert(x.BeatmapSet != null);
Debug.Assert(y.BeatmapSet != null); Debug.Assert(y.BeatmapSet != null);
string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.Metadata))?.File.Hash; string? fileHashX = x.BeatmapSet.GetFile(getFilename(x.Metadata))?.File.Hash;
string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.Metadata))?.File.Hash; string? fileHashY = y.BeatmapSet.GetFile(getFilename(y.Metadata))?.File.Hash;
return fileHashX == fileHashY; return fileHashX == fileHashY;
} }

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.Linq; using System.Linq;
using osu.Framework.Localisation; using osu.Framework.Localisation;

View File

@ -300,7 +300,7 @@ namespace osu.Game.Beatmaps
stream.Seek(0, SeekOrigin.Begin); stream.Seek(0, SeekOrigin.Begin);
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity. // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)); var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo); string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
// ensure that two difficulties from the set don't point at the same beatmap file. // ensure that two difficulties from the set don't point at the same beatmap file.

View File

@ -84,13 +84,6 @@ namespace osu.Game.Beatmaps
{ {
} }
/// <summary>
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
/// </summary>
/// <param name="filename">The name of the file to get the storage path of.</param>
public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath();
public bool Equals(BeatmapSetInfo? other) public bool Equals(BeatmapSetInfo? other)
{ {
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;

View File

@ -0,0 +1,33 @@
// 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.Linq;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Models;
namespace osu.Game.Beatmaps
{
public static class BeatmapSetInfoExtensions
{
/// <summary>
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
/// The lookup is case insensitive.
/// </summary>
/// <param name="model">The model to operate on.</param>
/// <param name="filename">The name of the file to get the storage path of.</param>
public static string? GetPathForFile(this IHasRealmFiles model, string filename) => model.GetFile(filename)?.File.GetStoragePath();
/// <summary>
/// Returns the file usage for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
/// The lookup is case insensitive.
/// </summary>
/// <param name="model">The model to operate on.</param>
/// <param name="filename">The name of the file to get the storage path of.</param>
public static RealmNamedFileUsage? GetFile(this IHasRealmFiles model, string filename) =>
model.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase));
}
}

View File

@ -3,16 +3,20 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum BackgroundSource public enum BackgroundSource
{ {
[LocalisableDescription(typeof(SkinSettingsStrings), nameof(SkinSettingsStrings.SkinSectionHeader))]
Skin, Skin,
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.BeatmapHeader))]
Beatmap, Beatmap,
[Description("Beatmap (with storyboard / video)")] [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))]
BeatmapWithStoryboard, BeatmapWithStoryboard,
} }
} }

View File

@ -3,17 +3,20 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum DiscordRichPresenceMode public enum DiscordRichPresenceMode
{ {
[LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.DiscordPresenceOff))]
Off, Off,
[Description("Hide identifiable information")] [LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.HideIdentifiableInformation))]
Limited, Limited,
[LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.DiscordPresenceFull))]
Full Full
} }
} }

View File

@ -3,17 +3,20 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum HUDVisibilityMode public enum HUDVisibilityMode
{ {
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.NeverShowHUD))]
Never, Never,
[Description("Hide during gameplay")] [LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.HideDuringGameplay))]
HideDuringGameplay, HideDuringGameplay,
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.AlwaysShowHUD))]
Always Always
} }
} }

View File

@ -3,16 +3,17 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum RandomSelectAlgorithm public enum RandomSelectAlgorithm
{ {
[Description("Never repeat")] [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.NeverRepeat))]
RandomPermutation, RandomPermutation,
[Description("True Random")] [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.TrueRandom))]
Random Random
} }
} }

View File

@ -3,17 +3,23 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum ScalingMode public enum ScalingMode
{ {
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScalingOff))]
Off, Off,
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleEverything))]
Everything, Everything,
[Description("Excluding overlays")] [LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleEverythingExcludingOverlays))]
ExcludeOverlays, ExcludeOverlays,
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleGameplay))]
Gameplay, Gameplay,
} }
} }

View File

@ -3,16 +3,17 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum ScreenshotFormat public enum ScreenshotFormat
{ {
[Description("JPG (web-friendly)")] [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.Jpg))]
Jpg = 1, Jpg = 1,
[Description("PNG (lossless)")] [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.Png))]
Png = 2 Png = 2
} }
} }

View File

@ -3,6 +3,9 @@
#nullable disable #nullable disable
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum SeasonalBackgroundMode public enum SeasonalBackgroundMode
@ -10,16 +13,19 @@ namespace osu.Game.Configuration
/// <summary> /// <summary>
/// Seasonal backgrounds are shown regardless of season, if at all available. /// Seasonal backgrounds are shown regardless of season, if at all available.
/// </summary> /// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.AlwaysSeasonalBackground))]
Always, Always,
/// <summary> /// <summary>
/// Seasonal backgrounds are shown only during their corresponding season. /// Seasonal backgrounds are shown only during their corresponding season.
/// </summary> /// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.SometimesSeasonalBackground))]
Sometimes, Sometimes,
/// <summary> /// <summary>
/// Seasonal backgrounds are never shown. /// Seasonal backgrounds are never shown.
/// </summary> /// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.NeverSeasonalBackground))]
Never Never
} }
} }

View File

@ -2,6 +2,7 @@
// 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.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Models; using osu.Game.Models;
namespace osu.Game.Database namespace osu.Game.Database
@ -11,8 +12,16 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public interface IHasRealmFiles public interface IHasRealmFiles
{ {
/// <summary>
/// Available files in this model, with locally filenames.
/// When performing lookups, consider using <see cref="BeatmapSetInfoExtensions.GetFile"/> or <see cref="BeatmapSetInfoExtensions.GetPathForFile"/> to do case-insensitive lookups.
/// </summary>
IList<RealmNamedFileUsage> Files { get; } IList<RealmNamedFileUsage> Files { get; }
/// <summary>
/// A combined hash representing the model, based on the files it contains.
/// Implementation specific.
/// </summary>
string Hash { get; set; } string Hash { get; set; }
} }
} }

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.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
@ -27,27 +25,30 @@ namespace osu.Game.Database
public class LegacyImportManager : Component public class LegacyImportManager : Component
{ {
[Resolved] [Resolved]
private SkinManager skins { get; set; } private SkinManager skins { get; set; } = null!;
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; } = null!;
[Resolved] [Resolved]
private ScoreManager scores { get; set; } private ScoreManager scores { get; set; } = null!;
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
[Resolved] [Resolved]
private IDialogOverlay dialogOverlay { get; set; } private OsuGame? game { get; set; }
[Resolved] [Resolved]
private RealmAccess realmAccess { get; set; } private IDialogOverlay dialogOverlay { get; set; } = null!;
[Resolved(canBeNull: true)] [Resolved]
private DesktopGameHost desktopGameHost { get; set; } private RealmAccess realmAccess { get; set; } = null!;
private StableStorage cachedStorage; [Resolved(canBeNull: true)] // canBeNull required while we remain on mono for mobile platforms.
private DesktopGameHost? desktopGameHost { get; set; }
[Resolved]
private INotificationOverlay? notifications { get; set; }
private StableStorage? cachedStorage;
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop; public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
@ -98,6 +99,9 @@ namespace osu.Game.Database
stableStorage = GetCurrentStableStorage(); stableStorage = GetCurrentStableStorage();
} }
if (stableStorage == null)
return;
var importTasks = new List<Task>(); var importTasks = new List<Task>();
Task beatmapImportTask = Task.CompletedTask; Task beatmapImportTask = Task.CompletedTask;
@ -108,7 +112,14 @@ namespace osu.Game.Database
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage)); importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
if (content.HasFlagFast(StableContent.Collections)) if (content.HasFlagFast(StableContent.Collections))
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess).ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); {
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess)
{
// Other legacy importers import via model managers which handle the posting of notifications.
// Collections are an exception.
PostNotification = n => notifications?.Post(n)
}.ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
}
if (content.HasFlagFast(StableContent.Scores)) if (content.HasFlagFast(StableContent.Scores))
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
@ -116,7 +127,7 @@ namespace osu.Game.Database
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);
} }
public StableStorage GetCurrentStableStorage() public StableStorage? GetCurrentStableStorage()
{ {
if (cachedStorage != null) if (cachedStorage != null)
return cachedStorage; return cachedStorage;

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
@ -79,7 +80,7 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public void AddFile(TModel item, Stream contents, string filename, Realm realm) public void AddFile(TModel item, Stream contents, string filename, Realm realm)
{ {
var existing = item.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase)); var existing = item.GetFile(filename);
if (existing != null) if (existing != null)
{ {

View File

@ -173,6 +173,11 @@ namespace osu.Game.Database
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
Filename += realm_extension; Filename += realm_extension;
#if DEBUG
if (!DebugUtils.IsNUnitRunning)
applyFilenameSchemaSuffix(ref Filename);
#endif
string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}"; string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
// Attempt to recover a newer database version if available. // Attempt to recover a newer database version if available.
@ -212,6 +217,51 @@ namespace osu.Game.Database
} }
} }
/// <summary>
/// Some developers may be annoyed if a newer version migration (ie. caused by testing a pull request)
/// cause their test database to be unusable with previous versions.
/// To get around this, store development databases against their realm version.
/// Note that this means changes made on newer realm versions will disappear.
/// </summary>
private void applyFilenameSchemaSuffix(ref string filename)
{
string originalFilename = filename;
filename = getVersionedFilename(schema_version);
// First check if the current realm version already exists...
if (storage.Exists(filename))
return;
// Check for a previous version we can use as a base database to migrate from...
for (int i = schema_version - 1; i >= 0; i--)
{
string previousFilename = getVersionedFilename(i);
if (storage.Exists(previousFilename))
{
copyPreviousVersion(previousFilename, filename);
return;
}
}
// Finally, check for a non-versioned file exists (aka before this method was added)...
if (storage.Exists(originalFilename))
copyPreviousVersion(originalFilename, filename);
void copyPreviousVersion(string previousFilename, string newFilename)
{
using (var previous = storage.GetStream(previousFilename))
using (var current = storage.CreateFileSafely(newFilename))
{
Logger.Log(@$"Copying previous realm database {previousFilename} to {newFilename} for migration to schema version {schema_version}");
previous.CopyTo(current);
}
}
string getVersionedFilename(int version) => originalFilename.Replace(realm_extension, $"_{version}{realm_extension}");
}
private void attemptRecoverFromFile(string recoveryFilename) private void attemptRecoverFromFile(string recoveryFilename)
{ {
Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database); Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database);

View File

@ -167,6 +167,11 @@ namespace osu.Game.Graphics.UserInterface
{ {
base.Update(); base.Update();
// If the game goes into a suspended state (ie. debugger attached or backgrounded on a mobile device)
// we want to ignore really long periods of no processing.
if (updateClock.ElapsedFrameTime > 10000)
return;
mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth); mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth);
// Handle the case where the window has become inactive or the user changed the // Handle the case where the window has become inactive or the user changed the
@ -177,15 +182,15 @@ namespace osu.Game.Graphics.UserInterface
// use elapsed frame time rather then FramesPerSecond to better catch stutter frames. // use elapsed frame time rather then FramesPerSecond to better catch stutter frames.
bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms; bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms;
// note that we use an elapsed time here of 1 intentionally. const float damp_time = 100;
// this weights all updates equally. if we passed in the elapsed time, longer frames would be weighted incorrectly lower.
displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : 100, 1); displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : damp_time, updateClock.ElapsedFrameTime);
if (hasDrawSpike) if (hasDrawSpike)
// show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show. // show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show.
displayedFpsCount = 1000 / drawClock.ElapsedFrameTime; displayedFpsCount = 1000 / drawClock.ElapsedFrameTime;
else else
displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, 100, Time.Elapsed); displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, damp_time, Time.Elapsed);
if (Time.Current - lastUpdate > min_time_between_updates) if (Time.Current - lastUpdate > min_time_between_updates)
{ {

View File

@ -20,6 +20,8 @@ namespace osu.Game.Graphics.UserInterface
/// </summary> /// </summary>
public class LoadingLayer : LoadingSpinner public class LoadingLayer : LoadingSpinner
{ {
private readonly bool blockInput;
[CanBeNull] [CanBeNull]
protected Box BackgroundDimLayer { get; } protected Box BackgroundDimLayer { get; }
@ -28,9 +30,11 @@ namespace osu.Game.Graphics.UserInterface
/// </summary> /// </summary>
/// <param name="dimBackground">Whether the full background area should be dimmed while loading.</param> /// <param name="dimBackground">Whether the full background area should be dimmed while loading.</param>
/// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param> /// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param>
public LoadingLayer(bool dimBackground = false, bool withBox = true) /// <param name="blockInput">Whether to block input of components behind the loading layer.</param>
public LoadingLayer(bool dimBackground = false, bool withBox = true, bool blockInput = true)
: base(withBox) : base(withBox)
{ {
this.blockInput = blockInput;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Size = new Vector2(1); Size = new Vector2(1);
@ -52,6 +56,9 @@ namespace osu.Game.Graphics.UserInterface
protected override bool Handle(UIEvent e) protected override bool Handle(UIEvent e)
{ {
if (!blockInput)
return false;
switch (e) switch (e)
{ {
// blocking scroll can cause weird behaviour when this layer is used within a ScrollContainer. // blocking scroll can cause weird behaviour when this layer is used within a ScrollContainer.
@ -83,7 +90,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
base.Update(); base.Update();
MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 30, 100)); MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 20, 100));
} }
} }
} }

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.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -15,10 +13,11 @@ namespace osu.Game.Input.Bindings
{ {
public class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput public class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput
{ {
private readonly Drawable handler; private readonly Drawable? handler;
private InputManager parentInputManager;
public GlobalActionContainer(OsuGameBase game) private InputManager? parentInputManager;
public GlobalActionContainer(OsuGameBase? game)
: base(matchingMode: KeyCombinationMatchingMode.Modifiers) : base(matchingMode: KeyCombinationMatchingMode.Modifiers)
{ {
if (game is IKeyBindingHandler<GlobalAction>) if (game is IKeyBindingHandler<GlobalAction>)
@ -32,7 +31,10 @@ namespace osu.Game.Input.Bindings
parentInputManager = GetContainingInputManager(); parentInputManager = GetContainingInputManager();
} }
// IMPORTANT: Do not change the order of key bindings in this list.
// It is used to decide the order of precedence (see note in DatabasedKeyBindingContainer).
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
.Concat(OverlayKeyBindings)
.Concat(EditorKeyBindings) .Concat(EditorKeyBindings)
.Concat(InGameKeyBindings) .Concat(InGameKeyBindings)
.Concat(SongSelectKeyBindings) .Concat(SongSelectKeyBindings)
@ -40,25 +42,6 @@ namespace osu.Game.Input.Bindings
public IEnumerable<KeyBinding> GlobalKeyBindings => new[] public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
{ {
new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying),
new KeyBinding(InputKey.F8, GlobalAction.ToggleChat),
new KeyBinding(InputKey.F9, GlobalAction.ToggleSocial),
new KeyBinding(InputKey.F10, GlobalAction.ToggleGameplayMouseButtons),
new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
new KeyBinding(InputKey.Escape, GlobalAction.Back),
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home),
new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious), new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious),
new KeyBinding(InputKey.Down, GlobalAction.SelectNext), new KeyBinding(InputKey.Down, GlobalAction.SelectNext),
@ -69,7 +52,32 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.Enter, GlobalAction.Select), new KeyBinding(InputKey.Enter, GlobalAction.Select),
new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select), new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select),
new KeyBinding(InputKey.Escape, GlobalAction.Back),
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay),
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.R }, GlobalAction.RandomSkin), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.R }, GlobalAction.RandomSkin),
new KeyBinding(InputKey.F10, GlobalAction.ToggleGameplayMouseButtons),
new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot),
};
public IEnumerable<KeyBinding> OverlayKeyBindings => new[]
{
new KeyBinding(InputKey.F8, GlobalAction.ToggleChat),
new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying),
new KeyBinding(InputKey.F9, GlobalAction.ToggleSocial),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
}; };
public IEnumerable<KeyBinding> EditorKeyBindings => new[] public IEnumerable<KeyBinding> EditorKeyBindings => new[]
@ -332,5 +340,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleFPSCounter))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleFPSCounter))]
ToggleFPSDisplay, ToggleFPSDisplay,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))]
ToggleProfile,
} }
} }

View File

@ -3,8 +3,9 @@
#nullable disable #nullable disable
using System.ComponentModel;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Input namespace osu.Game.Input
{ {
@ -17,18 +18,20 @@ namespace osu.Game.Input
/// <summary> /// <summary>
/// The mouse cursor will be free to move outside the game window. /// The mouse cursor will be free to move outside the game window.
/// </summary> /// </summary>
[LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.NeverConfine))]
Never, Never,
/// <summary> /// <summary>
/// The mouse cursor will be locked to the window bounds during gameplay, /// The mouse cursor will be locked to the window bounds during gameplay,
/// but may otherwise move freely. /// but may otherwise move freely.
/// </summary> /// </summary>
[Description("During Gameplay")] [LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.ConfineDuringGameplay))]
DuringGameplay, DuringGameplay,
/// <summary> /// <summary>
/// The mouse cursor will always be locked to the window bounds while the game has focus. /// The mouse cursor will always be locked to the window bounds while the game has focus.
/// </summary> /// </summary>
[LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.AlwaysConfine))]
Always Always
} }
} }

View File

@ -101,4 +101,4 @@ namespace osu.Game.Localisation
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -104,6 +104,31 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString IncreaseFirstObjectVisibility => new TranslatableString(getKey(@"increase_first_object_visibility"), @"Increase visibility of first object when visual impairment mods are enabled"); public static LocalisableString IncreaseFirstObjectVisibility => new TranslatableString(getKey(@"increase_first_object_visibility"), @"Increase visibility of first object when visual impairment mods are enabled");
/// <summary>
/// "Hide during gameplay"
/// </summary>
public static LocalisableString HideDuringGameplay => new TranslatableString(getKey(@"hide_during_gameplay"), @"Hide during gameplay");
/// <summary>
/// "Always"
/// </summary>
public static LocalisableString AlwaysShowHUD => new TranslatableString(getKey(@"always_show_hud"), @"Always");
/// <summary>
/// "Never"
/// </summary>
public static LocalisableString NeverShowHUD => new TranslatableString(getKey(@"never_show_hud"), @"Never");
/// <summary>
/// "Standardised"
/// </summary>
public static LocalisableString StandardisedScoreDisplay => new TranslatableString(getKey(@"standardised_score_display"), @"Standardised");
/// <summary>
/// "Classic"
/// </summary>
public static LocalisableString ClassicScoreDisplay => new TranslatableString(getKey(@"classic_score_display"), @"Classic");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -149,6 +149,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ToggleNotifications => new TranslatableString(getKey(@"toggle_notifications"), @"Toggle notifications"); public static LocalisableString ToggleNotifications => new TranslatableString(getKey(@"toggle_notifications"), @"Toggle notifications");
/// <summary>
/// "Toggle profile"
/// </summary>
public static LocalisableString ToggleProfile => new TranslatableString(getKey(@"toggle_profile"), @"Toggle profile");
/// <summary> /// <summary>
/// "Pause gameplay" /// "Pause gameplay"
/// </summary> /// </summary>

View File

@ -129,6 +129,16 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString UseHardwareAcceleration => new TranslatableString(getKey(@"use_hardware_acceleration"), @"Use hardware acceleration"); public static LocalisableString UseHardwareAcceleration => new TranslatableString(getKey(@"use_hardware_acceleration"), @"Use hardware acceleration");
/// <summary>
/// "JPG (web-friendly)"
/// </summary>
public static LocalisableString Jpg => new TranslatableString(getKey(@"jpg_web_friendly"), @"JPG (web-friendly)");
/// <summary>
/// "PNG (lossless)"
/// </summary>
public static LocalisableString Png => new TranslatableString(getKey(@"png_lossless"), @"PNG (lossless)");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString GlobalKeyBindingHeader => new TranslatableString(getKey(@"global_key_binding_header"), @"Global"); public static LocalisableString GlobalKeyBindingHeader => new TranslatableString(getKey(@"global_key_binding_header"), @"Global");
/// <summary>
/// "Overlays"
/// </summary>
public static LocalisableString OverlaysSection => new TranslatableString(getKey(@"overlays_section"), @"Overlays");
/// <summary> /// <summary>
/// "Song Select" /// "Song Select"
/// </summary> /// </summary>

View File

@ -29,6 +29,26 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString FullscreenMacOSNote => new TranslatableString(getKey(@"fullscreen_macos_note"), @"Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended."); public static LocalisableString FullscreenMacOSNote => new TranslatableString(getKey(@"fullscreen_macos_note"), @"Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended.");
/// <summary>
/// "Excluding overlays"
/// </summary>
public static LocalisableString ScaleEverythingExcludingOverlays => new TranslatableString(getKey(@"scale_everything_excluding_overlays"), @"Excluding overlays");
/// <summary>
/// "Everything"
/// </summary>
public static LocalisableString ScaleEverything => new TranslatableString(getKey(@"scale_everything"), @"Everything");
/// <summary>
/// "Gameplay"
/// </summary>
public static LocalisableString ScaleGameplay => new TranslatableString(getKey(@"scale_gameplay"), @"Gameplay");
/// <summary>
/// "Off"
/// </summary>
public static LocalisableString ScalingOff => new TranslatableString(getKey(@"scaling_off"), @"Off");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -64,6 +64,21 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString HighPrecisionPlatformWarning => new TranslatableString(getKey(@"high_precision_platform_warning"), @"This setting has known issues on your platform. If you encounter problems, it is recommended to adjust sensitivity externally and keep this disabled for now."); public static LocalisableString HighPrecisionPlatformWarning => new TranslatableString(getKey(@"high_precision_platform_warning"), @"This setting has known issues on your platform. If you encounter problems, it is recommended to adjust sensitivity externally and keep this disabled for now.");
/// <summary>
/// "Always"
/// </summary>
public static LocalisableString AlwaysConfine => new TranslatableString(getKey(@"always_confine"), @"Always");
/// <summary>
/// "During Gameplay"
/// </summary>
public static LocalisableString ConfineDuringGameplay => new TranslatableString(getKey(@"confine_during_gameplay"), @"During Gameplay");
/// <summary>
/// "Never"
/// </summary>
public static LocalisableString NeverConfine => new TranslatableString(getKey(@"never_confine"), @"Never");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -64,6 +64,21 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ShowExplicitContent => new TranslatableString(getKey(@"show_explicit_content"), @"Show explicit content in search results"); public static LocalisableString ShowExplicitContent => new TranslatableString(getKey(@"show_explicit_content"), @"Show explicit content in search results");
/// <summary>
/// "Hide identifiable information"
/// </summary>
public static LocalisableString HideIdentifiableInformation => new TranslatableString(getKey(@"hide_identifiable_information"), @"Hide identifiable information");
/// <summary>
/// "Full"
/// </summary>
public static LocalisableString DiscordPresenceFull => new TranslatableString(getKey(@"discord_presence_full"), @"Full");
/// <summary>
/// "Off"
/// </summary>
public static LocalisableString DiscordPresenceOff => new TranslatableString(getKey(@"discord_presence_off"), @"Off");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -14,6 +14,21 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets"); public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets");
/// <summary>
/// "None"
/// </summary>
public static LocalisableString BorderNone => new TranslatableString(getKey(@"no_borders"), @"None");
/// <summary>
/// "Corners"
/// </summary>
public static LocalisableString BorderCorners => new TranslatableString(getKey(@"corner_borders"), @"Corners");
/// <summary>
/// "Full"
/// </summary>
public static LocalisableString BorderFull => new TranslatableString(getKey(@"full_borders"), @"Full");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class ToolbarStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.Toolbar";
/// <summary>
/// "Connection interrupted, will try to reconnect..."
/// </summary>
public static LocalisableString AttemptingToReconnect => new TranslatableString(getKey(@"attempting_to_reconnect"), @"Connection interrupted, will try to reconnect...");
/// <summary>
/// "Connecting..."
/// </summary>
public static LocalisableString Connecting => new TranslatableString(getKey(@"connecting"), @"Connecting...");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -114,6 +114,46 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString NoLimit => new TranslatableString(getKey(@"no_limit"), @"no limit"); public static LocalisableString NoLimit => new TranslatableString(getKey(@"no_limit"), @"no limit");
/// <summary>
/// "Beatmap (with storyboard / video)"
/// </summary>
public static LocalisableString BeatmapWithStoryboard => new TranslatableString(getKey(@"beatmap_with_storyboard"), @"Beatmap (with storyboard / video)");
/// <summary>
/// "Always"
/// </summary>
public static LocalisableString AlwaysSeasonalBackground => new TranslatableString(getKey(@"always_seasonal_backgrounds"), @"Always");
/// <summary>
/// "Never"
/// </summary>
public static LocalisableString NeverSeasonalBackground => new TranslatableString(getKey(@"never_seasonal_backgrounds"), @"Never");
/// <summary>
/// "Sometimes"
/// </summary>
public static LocalisableString SometimesSeasonalBackground => new TranslatableString(getKey(@"sometimes_seasonal_backgrounds"), @"Sometimes");
/// <summary>
/// "Sequential"
/// </summary>
public static LocalisableString SequentialHotkeyStyle => new TranslatableString(getKey(@"mods_sequential_hotkeys"), @"Sequential");
/// <summary>
/// "Classic"
/// </summary>
public static LocalisableString ClassicHotkeyStyle => new TranslatableString(getKey(@"mods_classic_hotkeys"), @"Classic");
/// <summary>
/// "Never repeat"
/// </summary>
public static LocalisableString NeverRepeat => new TranslatableString(getKey(@"never_repeat_random"), @"Never repeat");
/// <summary>
/// "True Random"
/// </summary>
public static LocalisableString TrueRandom => new TranslatableString(getKey(@"true_random"), @"True Random");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -104,120 +104,39 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
private int failureCount; private int failureCount;
/// <summary>
/// The main API thread loop, which will continue to run until the game is shut down.
/// </summary>
private void run() private void run()
{ {
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested)
{ {
switch (State.Value) if (state.Value == APIState.Failing)
{ {
case APIState.Failing: // To recover from a failing state, falling through and running the full reconnection process seems safest for now.
//todo: replace this with a ping request. // This could probably be replaced with a ping-style request if we want to avoid the reconnection overheads.
log.Add(@"In a failing state, waiting a bit before we try again..."); log.Add($@"{nameof(APIAccess)} is in a failing state, waiting a bit before we try again...");
Thread.Sleep(5000); Thread.Sleep(5000);
}
if (!IsLoggedIn) goto case APIState.Connecting; // Ensure that we have valid credentials.
// If not, setting the offline state will allow the game to prompt the user to provide new credentials.
if (!HasLogin)
{
state.Value = APIState.Offline;
Thread.Sleep(50);
continue;
}
if (queue.Count == 0) Debug.Assert(HasLogin);
{
log.Add(@"Queueing a ping request");
Queue(new GetUserRequest());
}
break; // Ensure that we are in an online state. If not, attempt a connect.
if (state.Value != APIState.Online)
{
attemptConnect();
case APIState.Offline: if (state.Value != APIState.Online)
case APIState.Connecting: continue;
// work to restore a connection...
if (!HasLogin)
{
state.Value = APIState.Offline;
Thread.Sleep(50);
continue;
}
state.Value = APIState.Connecting;
// save the username at this point, if the user requested for it to be.
config.SetValue(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
if (!authentication.HasValidAccessToken)
{
LastLoginError = null;
try
{
authentication.AuthenticateWithLogin(ProvidedUsername, password);
}
catch (Exception e)
{
//todo: this fails even on network-related issues. we should probably handle those differently.
LastLoginError = e;
log.Add(@"Login failed!");
password = null;
authentication.Clear();
continue;
}
}
var userReq = new GetUserRequest();
userReq.Failure += ex =>
{
if (ex is APIException)
{
LastLoginError = ex;
log.Add("Login failed on local user retrieval!");
Logout();
}
else if (ex is WebException webException && webException.Message == @"Unauthorized")
{
log.Add(@"Login no longer valid");
Logout();
}
else
failConnectionProcess();
};
userReq.Success += u =>
{
localUser.Value = u;
// todo: save/pull from settings
localUser.Value.Status.Value = new UserStatusOnline();
failureCount = 0;
};
if (!handleRequest(userReq))
{
failConnectionProcess();
continue;
}
// getting user's friends is considered part of the connection process.
var friendsReq = new GetFriendsRequest();
friendsReq.Failure += _ => failConnectionProcess();
friendsReq.Success += res =>
{
friends.AddRange(res);
//we're connected!
state.Value = APIState.Online;
};
if (!handleRequest(friendsReq))
{
failConnectionProcess();
continue;
}
// The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
// Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
// before actually going online.
while (State.Value > APIState.Offline && State.Value < APIState.Online)
Thread.Sleep(500);
break;
} }
// hard bail if we can't get a valid access token. // hard bail if we can't get a valid access token.
@ -227,31 +146,132 @@ namespace osu.Game.Online.API
continue; continue;
} }
while (true) processQueuedRequests();
{
APIRequest req;
lock (queue)
{
if (queue.Count == 0) break;
req = queue.Dequeue();
}
handleRequest(req);
}
Thread.Sleep(50); Thread.Sleep(50);
} }
}
void failConnectionProcess() /// <summary>
/// Dequeue from the queue and run each request synchronously until the queue is empty.
/// </summary>
private void processQueuedRequests()
{
while (true)
{ {
// if something went wrong during the connection process, we want to reset the state (but only if still connecting). APIRequest req;
if (State.Value == APIState.Connecting)
state.Value = APIState.Failing; lock (queue)
{
if (queue.Count == 0) return;
req = queue.Dequeue();
}
handleRequest(req);
} }
} }
/// <summary>
/// From a non-connected state, perform a full connection flow, obtaining OAuth tokens and populating the local user and friends.
/// </summary>
/// <remarks>
/// This method takes control of <see cref="state"/> and transitions from <see cref="APIState.Connecting"/> to either
/// - <see cref="APIState.Online"/> (successful connection)
/// - <see cref="APIState.Failing"/> (failed connection but retrying)
/// - <see cref="APIState.Offline"/> (failed and can't retry, clear credentials and require user interaction)
/// </remarks>
/// <returns>Whether the connection attempt was successful.</returns>
private void attemptConnect()
{
state.Value = APIState.Connecting;
if (localUser.IsDefault)
{
// Show a placeholder user if saved credentials are available.
// This is useful for storing local scores and showing a placeholder username after starting the game,
// until a valid connection has been established.
setLocalUser(new APIUser
{
Username = ProvidedUsername,
});
}
// save the username at this point, if the user requested for it to be.
config.SetValue(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
if (!authentication.HasValidAccessToken)
{
LastLoginError = null;
try
{
authentication.AuthenticateWithLogin(ProvidedUsername, password);
}
catch (Exception e)
{
//todo: this fails even on network-related issues. we should probably handle those differently.
LastLoginError = e;
log.Add($@"Login failed for username {ProvidedUsername} ({LastLoginError.Message})!");
Logout();
return;
}
}
var userReq = new GetUserRequest();
userReq.Failure += ex =>
{
if (ex is APIException)
{
LastLoginError = ex;
log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
Logout();
}
else if (ex is WebException webException && webException.Message == @"Unauthorized")
{
log.Add(@"Login no longer valid");
Logout();
}
else
{
state.Value = APIState.Failing;
}
};
userReq.Success += user =>
{
// todo: save/pull from settings
user.Status.Value = new UserStatusOnline();
setLocalUser(user);
// we're connected!
state.Value = APIState.Online;
failureCount = 0;
};
if (!handleRequest(userReq))
{
state.Value = APIState.Failing;
return;
}
var friendsReq = new GetFriendsRequest();
friendsReq.Failure += _ => state.Value = APIState.Failing;
friendsReq.Success += res => friends.AddRange(res);
if (!handleRequest(friendsReq))
{
state.Value = APIState.Failing;
return;
}
// The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
// Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
// before actually going online.
while (State.Value == APIState.Connecting && !cancellationToken.IsCancellationRequested)
Thread.Sleep(500);
}
public void Perform(APIRequest request) public void Perform(APIRequest request)
{ {
try try
@ -327,8 +347,7 @@ namespace osu.Game.Online.API
if (req.CompletionState != APIRequestCompletionState.Completed) if (req.CompletionState != APIRequestCompletionState.Completed)
return false; return false;
// we could still be in initialisation, at which point we don't want to say we're Online yet. // Reset failure count if this request succeeded.
if (IsLoggedIn) state.Value = APIState.Online;
failureCount = 0; failureCount = 0;
return true; return true;
} }
@ -402,7 +421,7 @@ namespace osu.Game.Online.API
} }
} }
public bool IsLoggedIn => localUser.Value.Id > 1; // TODO: should this also be true if attempting to connect? public bool IsLoggedIn => State.Value > APIState.Offline;
public void Queue(APIRequest request) public void Queue(APIRequest request)
{ {
@ -442,7 +461,7 @@ namespace osu.Game.Online.API
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
Schedule(() => Schedule(() =>
{ {
localUser.Value = createGuestUser(); setLocalUser(createGuestUser());
friends.Clear(); friends.Clear();
}); });
@ -452,6 +471,8 @@ namespace osu.Game.Online.API
private static APIUser createGuestUser() => new GuestUser(); private static APIUser createGuestUser() => new GuestUser();
private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -13,19 +13,16 @@ namespace osu.Game.Online.API
{ {
/// <summary> /// <summary>
/// The local user. /// The local user.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary> /// </summary>
IBindable<APIUser> LocalUser { get; } IBindable<APIUser> LocalUser { get; }
/// <summary> /// <summary>
/// The user's friends. /// The user's friends.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary> /// </summary>
IBindableList<APIUser> Friends { get; } IBindableList<APIUser> Friends { get; }
/// <summary> /// <summary>
/// The current user's activity. /// The current user's activity.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary> /// </summary>
IBindable<UserActivity> Activity { get; } IBindable<UserActivity> Activity { get; }

View File

@ -196,6 +196,9 @@ namespace osu.Game.Online.Multiplayer
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem)); APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
// The server will null out the end date upon the host joining the room, but the null value is never communicated to the client.
APIRoom.EndDate.Value = null;
Debug.Assert(LocalUser != null); Debug.Assert(LocalUser != null);
addUserToAPIRoom(LocalUser); addUserToAPIRoom(LocalUser);

View File

@ -1138,6 +1138,13 @@ namespace osu.Game
mouseDisableButtons.Value = !mouseDisableButtons.Value; mouseDisableButtons.Value = !mouseDisableButtons.Value;
return true; return true;
case GlobalAction.ToggleProfile:
if (userProfile.State.Value == Visibility.Visible)
userProfile.Hide();
else
ShowUser(API.LocalUser.Value);
return true;
case GlobalAction.RandomSkin: case GlobalAction.RandomSkin:
// Don't allow random skin selection while in the skin editor. // Don't allow random skin selection while in the skin editor.
// This is mainly to stop many "osu! default (modified)" skins being created via the SkinManager.EnsureMutableSkin() path. // This is mainly to stop many "osu! default (modified)" skins being created via the SkinManager.EnsureMutableSkin() path.

View File

@ -104,11 +104,11 @@ namespace osu.Game.Overlays
filterControl.CardSize.BindValueChanged(_ => onCardSizeChanged()); filterControl.CardSize.BindValueChanged(_ => onCardSizeChanged());
apiUser = api.LocalUser.GetBoundCopy(); apiUser = api.LocalUser.GetBoundCopy();
apiUser.BindValueChanged(_ => apiUser.BindValueChanged(_ => Schedule(() =>
{ {
if (api.IsLoggedIn) if (api.IsLoggedIn)
addContentToResultsArea(Drawable.Empty()); addContentToResultsArea(Drawable.Empty());
}); }));
} }
public void ShowWithSearch(string query) public void ShowWithSearch(string query)

View File

@ -4,7 +4,6 @@
#nullable disable #nullable disable
using Markdig.Syntax; using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Containers.Markdown;
using osu.Game.Graphics.Containers.Markdown; using osu.Game.Graphics.Containers.Markdown;
@ -12,16 +11,8 @@ namespace osu.Game.Overlays.Comments
{ {
public class CommentMarkdownContainer : OsuMarkdownContainer public class CommentMarkdownContainer : OsuMarkdownContainer
{ {
public override MarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer();
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock); protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock);
private class CommentMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
{
// Don't render image in comment for now
protected override void AddImage(LinkInline linkInline) { }
}
private class CommentMarkdownHeading : OsuMarkdownHeading private class CommentMarkdownHeading : OsuMarkdownHeading
{ {
public CommentMarkdownHeading(HeadingBlock headingBlock) public CommentMarkdownHeading(HeadingBlock headingBlock)

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Users; using osu.Game.Users;
using osuTK; using osuTK;
@ -109,7 +110,7 @@ namespace osu.Game.Overlays.Login
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
TextAnchor = Anchor.TopCentre, TextAnchor = Anchor.TopCentre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Text = state.NewValue == APIState.Failing ? "Connection is failing, will attempt to reconnect... " : "Attempting to connect... ", Text = state.NewValue == APIState.Failing ? ToolbarStrings.AttemptingToReconnect : ToolbarStrings.Connecting,
Margin = new MarginPadding { Top = 10, Bottom = 10 }, Margin = new MarginPadding { Top = 10, Bottom = 10 },
}, },
}; };

View File

@ -1,7 +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.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Mods.Input namespace osu.Game.Overlays.Mods.Input
{ {
@ -15,6 +17,7 @@ namespace osu.Game.Overlays.Mods.Input
/// Individual letters in a row trigger the mods in a sequential fashion. /// Individual letters in a row trigger the mods in a sequential fashion.
/// Uses <see cref="SequentialModHotkeyHandler"/>. /// Uses <see cref="SequentialModHotkeyHandler"/>.
/// </summary> /// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.SequentialHotkeyStyle))]
Sequential, Sequential,
/// <summary> /// <summary>
@ -22,6 +25,7 @@ namespace osu.Game.Overlays.Mods.Input
/// One keybinding can toggle between what used to be <see cref="MultiMod"/>s on stable, /// One keybinding can toggle between what used to be <see cref="MultiMod"/>s on stable,
/// and some mods in a column may not have any hotkeys at all. /// and some mods in a column may not have any hotkeys at all.
/// </summary> /// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.ClassicHotkeyStyle))]
Classic Classic
} }
} }

View File

@ -66,7 +66,7 @@ namespace osu.Game.Overlays.Mods
private IModHotkeyHandler hotkeyHandler = null!; private IModHotkeyHandler hotkeyHandler = null!;
private Task? latestLoadTask; private Task? latestLoadTask;
internal bool ItemsLoaded => latestLoadTask == null; internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true;
public ModColumn(ModType modType, bool allowIncompatibleSelection) public ModColumn(ModType modType, bool allowIncompatibleSelection)
{ {
@ -132,18 +132,11 @@ namespace osu.Game.Overlays.Mods
var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero)); var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero));
Task? loadTask; latestLoadTask = LoadComponentsAsync(panels, loaded =>
latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded =>
{ {
ItemsFlow.ChildrenEnumerable = loaded; ItemsFlow.ChildrenEnumerable = loaded;
updateState(); updateState();
}, (cancellationTokenSource = new CancellationTokenSource()).Token); }, (cancellationTokenSource = new CancellationTokenSource()).Token);
loadTask.ContinueWith(_ =>
{
if (loadTask == latestLoadTask)
latestLoadTask = null;
});
} }
private void updateState() private void updateState()

View File

@ -2,12 +2,12 @@
// 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 System;
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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Localisation; using osu.Game.Localisation;
@ -50,45 +50,41 @@ namespace osu.Game.Overlays.Mods
{ {
presetSubscription?.Dispose(); presetSubscription?.Dispose();
presetSubscription = realm.RegisterForNotifications(r => presetSubscription = realm.RegisterForNotifications(r =>
r.All<ModPreset>() r.All<ModPreset>()
.Filter($"{nameof(ModPreset.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $0" .Filter($"{nameof(ModPreset.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $0"
+ $" && {nameof(ModPreset.DeletePending)} == false", ruleset.Value.ShortName) + $" && {nameof(ModPreset.DeletePending)} == false", ruleset.Value.ShortName)
.OrderBy(preset => preset.Name), .OrderBy(preset => preset.Name), asyncLoadPanels);
(presets, _, _) => asyncLoadPanels(presets));
} }
private CancellationTokenSource? cancellationTokenSource; private CancellationTokenSource? cancellationTokenSource;
private Task? latestLoadTask; private Task? latestLoadTask;
internal bool ItemsLoaded => latestLoadTask == null; internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true;
private void asyncLoadPanels(IReadOnlyList<ModPreset> presets) private void asyncLoadPanels(IRealmCollection<ModPreset> presets, ChangeSet changes, Exception error)
{ {
cancellationTokenSource?.Cancel(); cancellationTokenSource?.Cancel();
if (!presets.Any()) if (!presets.Any())
{ {
ItemsFlow.RemoveAll(panel => panel is ModPresetPanel); removeAndDisposePresetPanels();
return; return;
} }
var panels = presets.Select(preset => new ModPresetPanel(preset.ToLive(realm)) latestLoadTask = LoadComponentsAsync(presets.Select(p => new ModPresetPanel(p.ToLive(realm))
{ {
Shear = Vector2.Zero Shear = Vector2.Zero
}); }), loaded =>
Task? loadTask;
latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded =>
{ {
ItemsFlow.RemoveAll(panel => panel is ModPresetPanel); removeAndDisposePresetPanels();
ItemsFlow.AddRange(loaded); ItemsFlow.AddRange(loaded);
}, (cancellationTokenSource = new CancellationTokenSource()).Token); }, (cancellationTokenSource = new CancellationTokenSource()).Token);
loadTask.ContinueWith(_ =>
void removeAndDisposePresetPanels()
{ {
if (loadTask == latestLoadTask) foreach (var panel in ItemsFlow.OfType<ModPresetPanel>().ToArray())
latestLoadTask = null; panel.RemoveAndDisposeImmediately();
}); }
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

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 osu.Framework.Bindables; using osu.Framework.Bindables;
@ -17,10 +15,12 @@ namespace osu.Game.Overlays.Music
{ {
public class Playlist : OsuRearrangeableListContainer<Live<BeatmapSetInfo>> public class Playlist : OsuRearrangeableListContainer<Live<BeatmapSetInfo>>
{ {
public Action<Live<BeatmapSetInfo>> RequestSelection; public Action<Live<BeatmapSetInfo>>? RequestSelection;
public readonly Bindable<Live<BeatmapSetInfo>> SelectedSet = new Bindable<Live<BeatmapSetInfo>>(); public readonly Bindable<Live<BeatmapSetInfo>> SelectedSet = new Bindable<Live<BeatmapSetInfo>>();
private FilterCriteria currentCriteria = new FilterCriteria();
public new MarginPadding Padding public new MarginPadding Padding
{ {
get => base.Padding; get => base.Padding;
@ -31,26 +31,22 @@ namespace osu.Game.Overlays.Music
{ {
var items = (SearchContainer<RearrangeableListItem<Live<BeatmapSetInfo>>>)ListContainer; var items = (SearchContainer<RearrangeableListItem<Live<BeatmapSetInfo>>>)ListContainer;
string[] currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray()); string[]? currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray());
foreach (var item in items.OfType<PlaylistItem>()) foreach (var item in items.OfType<PlaylistItem>())
{ {
if (currentCollectionHashes == null) item.InSelectedCollection = currentCollectionHashes == null || item.Model.Value.Beatmaps.Select(b => b.MD5Hash).Any(currentCollectionHashes.Contains);
item.InSelectedCollection = true;
else
{
item.InSelectedCollection = item.Model.Value.Beatmaps.Select(b => b.MD5Hash)
.Any(currentCollectionHashes.Contains);
}
} }
items.SearchTerm = criteria.SearchText; items.SearchTerm = criteria.SearchText;
currentCriteria = criteria;
} }
public Live<BeatmapSetInfo> FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); public Live<BeatmapSetInfo>? FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter);
protected override OsuRearrangeableListItem<Live<BeatmapSetInfo>> CreateOsuDrawable(Live<BeatmapSetInfo> item) => new PlaylistItem(item) protected override OsuRearrangeableListItem<Live<BeatmapSetInfo>> CreateOsuDrawable(Live<BeatmapSetInfo> item) => new PlaylistItem(item)
{ {
InSelectedCollection = currentCriteria.Collection?.PerformRead(c => item.Value.Beatmaps.Select(b => b.MD5Hash).Any(c.BeatmapMD5Hashes.Contains)) != false,
SelectedSet = { BindTarget = SelectedSet }, SelectedSet = { BindTarget = SelectedSet },
RequestSelection = set => RequestSelection?.Invoke(set) RequestSelection = set => RequestSelection?.Invoke(set)
}; };

View File

@ -26,8 +26,6 @@ namespace osu.Game.Overlays.Music
private const float transition_duration = 600; private const float transition_duration = 600;
private const float playlist_height = 510; private const float playlist_height = 510;
public IBindableList<Live<BeatmapSetInfo>> BeatmapSets => beatmapSets;
private readonly BindableList<Live<BeatmapSetInfo>> beatmapSets = new BindableList<Live<BeatmapSetInfo>>(); private readonly BindableList<Live<BeatmapSetInfo>> beatmapSets = new BindableList<Live<BeatmapSetInfo>>();
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>(); private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
@ -104,9 +102,7 @@ namespace osu.Game.Overlays.Music
{ {
base.LoadComplete(); base.LoadComplete();
// tests might bind externally, in which case we don't want to involve realm. beatmapSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
if (beatmapSets.Count == 0)
beatmapSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
list.Items.BindTo(beatmapSets); list.Items.BindTo(beatmapSets);
beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true); beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true);

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.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -23,6 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
public GlobalKeyBindingsSection(GlobalActionContainer manager) public GlobalKeyBindingsSection(GlobalActionContainer manager)
{ {
Add(new DefaultBindingsSubsection(manager)); Add(new DefaultBindingsSubsection(manager));
Add(new OverlayBindingsSubsection(manager));
Add(new AudioControlKeyBindingsSubsection(manager)); Add(new AudioControlKeyBindingsSubsection(manager));
Add(new SongSelectKeyBindingSubsection(manager)); Add(new SongSelectKeyBindingSubsection(manager));
Add(new InGameKeyBindingsSubsection(manager)); Add(new InGameKeyBindingsSubsection(manager));
@ -40,6 +39,17 @@ namespace osu.Game.Overlays.Settings.Sections.Input
} }
} }
private class OverlayBindingsSubsection : KeyBindingsSubsection
{
protected override LocalisableString Header => InputSettingsStrings.OverlaysSection;
public OverlayBindingsSubsection(GlobalActionContainer manager)
: base(null)
{
Defaults = manager.OverlayKeyBindings;
}
}
private class SongSelectKeyBindingSubsection : KeyBindingsSubsection private class SongSelectKeyBindingSubsection : KeyBindingsSubsection
{ {
protected override LocalisableString Header => InputSettingsStrings.SongSelectSection; protected override LocalisableString Header => InputSettingsStrings.SongSelectSection;

View File

@ -1,17 +1,19 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -20,59 +22,103 @@ namespace osu.Game.Overlays.Toolbar
{ {
public class ToolbarUserButton : ToolbarOverlayToggleButton public class ToolbarUserButton : ToolbarOverlayToggleButton
{ {
private readonly UpdateableAvatar avatar; private UpdateableAvatar avatar = null!;
[Resolved] private IBindable<APIUser> localUser = null!;
private IAPIProvider api { get; set; }
private readonly IBindable<APIState> apiState = new Bindable<APIState>(); private LoadingSpinner spinner = null!;
private SpriteIcon failingIcon = null!;
private IBindable<APIState> apiState = null!;
public ToolbarUserButton() public ToolbarUserButton()
{ {
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;
}
DrawableText.Font = OsuFont.GetFont(italics: true); [BackgroundDependencyLoader]
private void load(OsuColour colours, IAPIProvider api, LoginOverlay? login)
{
Add(new OpaqueBackground { Depth = 1 }); Add(new OpaqueBackground { Depth = 1 });
Flow.Add(avatar = new UpdateableAvatar(isInteractive: false) Flow.Add(new Container
{ {
Masking = true, Masking = true,
CornerRadius = 4,
Size = new Vector2(32), Size = new Vector2(32),
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
CornerRadius = 4,
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Shadow, Type = EdgeEffectType.Shadow,
Radius = 4, Radius = 4,
Colour = Color4.Black.Opacity(0.1f), Colour = Color4.Black.Opacity(0.1f),
},
Children = new Drawable[]
{
avatar = new UpdateableAvatar(isInteractive: false)
{
RelativeSizeAxes = Axes.Both,
},
spinner = new LoadingLayer(dimBackground: true, withBox: false, blockInput: false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
},
failingIcon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Size = new Vector2(0.3f),
Icon = FontAwesome.Solid.ExclamationTriangle,
RelativeSizeAxes = Axes.Both,
Colour = colours.YellowLight,
},
} }
}); });
}
[BackgroundDependencyLoader(true)] apiState = api.State.GetBoundCopy();
private void load(LoginOverlay login)
{
apiState.BindTo(api.State);
apiState.BindValueChanged(onlineStateChanged, true); apiState.BindValueChanged(onlineStateChanged, true);
localUser = api.LocalUser.GetBoundCopy();
localUser.BindValueChanged(userChanged, true);
StateContainer = login; StateContainer = login;
} }
private void userChanged(ValueChangedEvent<APIUser> user) => Schedule(() =>
{
Text = user.NewValue.Username;
avatar.User = user.NewValue;
});
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() => private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{ {
failingIcon.FadeTo(state.NewValue == APIState.Failing ? 1 : 0, 200, Easing.OutQuint);
switch (state.NewValue) switch (state.NewValue)
{ {
default: case APIState.Connecting:
Text = UsersStrings.AnonymousUsername; TooltipText = ToolbarStrings.Connecting;
avatar.User = new APIUser(); spinner.Show();
break; break;
case APIState.Online: case APIState.Failing:
Text = api.LocalUser.Value.Username; TooltipText = ToolbarStrings.AttemptingToReconnect;
avatar.User = api.LocalUser.Value; spinner.Show();
break; break;
case APIState.Offline:
case APIState.Online:
TooltipText = string.Empty;
spinner.Hide();
break;
default:
throw new ArgumentOutOfRangeException();
} }
}); });
} }

View File

@ -5,6 +5,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using osu.Game.Beatmaps;
using osu.Game.IO.FileAbstraction; using osu.Game.IO.FileAbstraction;
using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Storyboards; using osu.Game.Storyboards;

View File

@ -5,6 +5,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks namespace osu.Game.Rulesets.Edit.Checks

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 0.5;
public override bool ValidForMultiplayer => false; public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false; public override bool ValidForMultiplayerAsFreeMod => false;

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => FontAwesome.Solid.Hammer; public override IconUsage? Icon => FontAwesome.Solid.Hammer;
public override double ScoreMultiplier => 1.0; public override double ScoreMultiplier => 0.5;
public override bool RequiresConfiguration => true; public override bool RequiresConfiguration => true;

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "RX"; public override string Acronym => "RX";
public override IconUsage? Icon => OsuIcon.ModRelax; public override IconUsage? Icon => OsuIcon.ModRelax;
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 0.1;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) }; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) };
} }
} }

View File

@ -16,6 +16,8 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Rulesets.Scoring namespace osu.Game.Rulesets.Scoring
{ {
@ -636,7 +638,10 @@ namespace osu.Game.Rulesets.Scoring
public enum ScoringMode public enum ScoringMode
{ {
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.StandardisedScoreDisplay))]
Standardised, Standardised,
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.ClassicScoreDisplay))]
Classic Classic
} }
} }

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.UI
public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both };
public override IFrameStableClock FrameStableClock => frameStabilityContainer.FrameStableClock; public override IFrameStableClock FrameStableClock => frameStabilityContainer;
private bool frameStablePlayback = true; private bool frameStablePlayback = true;

View File

@ -1,16 +1,16 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -20,9 +20,11 @@ namespace osu.Game.Rulesets.UI
/// A container which consumes a parent gameplay clock and standardises frame counts for children. /// A container which consumes a parent gameplay clock and standardises frame counts for children.
/// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks. /// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks.
/// </summary> /// </summary>
public class FrameStabilityContainer : Container, IHasReplayHandler [Cached(typeof(IGameplayClock))]
[Cached(typeof(IFrameStableClock))]
public sealed class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock
{ {
private readonly double gameplayStartTime; public ReplayInputHandler? ReplayInputHandler { get; set; }
/// <summary> /// <summary>
/// The number of frames (per parent frame) which can be run in an attempt to catch-up to real-time. /// The number of frames (per parent frame) which can be run in an attempt to catch-up to real-time.
@ -32,28 +34,35 @@ namespace osu.Game.Rulesets.UI
/// <summary> /// <summary>
/// Whether to enable frame-stable playback. /// Whether to enable frame-stable playback.
/// </summary> /// </summary>
internal bool FrameStablePlayback = true; internal bool FrameStablePlayback { get; set; } = true;
public IFrameStableClock FrameStableClock => frameStableClock; protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid;
[Cached(typeof(GameplayClock))] private readonly Bindable<bool> isCatchingUp = new Bindable<bool>();
private readonly FrameStabilityClock frameStableClock;
public FrameStabilityContainer(double gameplayStartTime = double.MinValue) private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>();
{
RelativeSizeAxes = Axes.Both;
frameStableClock = new FrameStabilityClock(framedClock = new FramedClock(manualClock = new ManualClock())); private readonly double gameplayStartTime;
this.gameplayStartTime = gameplayStartTime; private IGameplayClock? parentGameplayClock;
}
/// <summary>
/// A clock which is used as reference for time, rate and running state.
/// </summary>
private IClock referenceClock = null!;
/// <summary>
/// A local manual clock which tracks the reference clock.
/// Values are transferred from <see cref="referenceClock"/> each update call.
/// </summary>
private readonly ManualClock manualClock; private readonly ManualClock manualClock;
/// <summary>
/// The main framed clock which has stability applied to it.
/// This gets exposed to children as an <see cref="IGameplayClock"/>.
/// </summary>
private readonly FramedClock framedClock; private readonly FramedClock framedClock;
private IFrameBasedClock parentGameplayClock;
/// <summary> /// <summary>
/// The current direction of playback to be exposed to frame stable children. /// The current direction of playback to be exposed to frame stable children.
/// </summary> /// </summary>
@ -62,32 +71,34 @@ namespace osu.Game.Rulesets.UI
/// </remarks> /// </remarks>
private int direction = 1; private int direction = 1;
[BackgroundDependencyLoader(true)]
private void load(GameplayClock clock)
{
if (clock != null)
{
parentGameplayClock = frameStableClock.ParentGameplayClock = clock;
frameStableClock.IsPaused.BindTo(clock.IsPaused);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
setClock();
}
private PlaybackState state; private PlaybackState state;
protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid;
private bool hasReplayAttached => ReplayInputHandler != null; private bool hasReplayAttached => ReplayInputHandler != null;
private const double sixty_frame_time = 1000.0 / 60;
private bool firstConsumption = true; private bool firstConsumption = true;
public FrameStabilityContainer(double gameplayStartTime = double.MinValue)
{
RelativeSizeAxes = Axes.Both;
framedClock = new FramedClock(manualClock = new ManualClock());
this.gameplayStartTime = gameplayStartTime;
}
[BackgroundDependencyLoader]
private void load(IGameplayClock? gameplayClock)
{
if (gameplayClock != null)
{
parentGameplayClock = gameplayClock;
IsPaused.BindTo(parentGameplayClock.IsPaused);
}
referenceClock = gameplayClock ?? Clock;
Clock = this;
}
public override bool UpdateSubTree() public override bool UpdateSubTree()
{ {
int loops = MaxCatchUpFrames; int loops = MaxCatchUpFrames;
@ -110,12 +121,12 @@ namespace osu.Game.Rulesets.UI
private void updateClock() private void updateClock()
{ {
if (frameStableClock.WaitingOnFrames.Value) if (waitingOnFrames.Value)
{ {
// if waiting on frames, run one update loop to determine if frames have arrived. // if waiting on frames, run one update loop to determine if frames have arrived.
state = PlaybackState.Valid; state = PlaybackState.Valid;
} }
else if (frameStableClock.IsPaused.Value) else if (IsPaused.Value)
{ {
// time should not advance while paused, nor should anything run. // time should not advance while paused, nor should anything run.
state = PlaybackState.NotValid; state = PlaybackState.NotValid;
@ -126,10 +137,7 @@ namespace osu.Game.Rulesets.UI
state = PlaybackState.Valid; state = PlaybackState.Valid;
} }
if (parentGameplayClock == null) double proposedTime = referenceClock.CurrentTime;
setClock(); // LoadComplete may not be run yet, but we still want the clock.
double proposedTime = parentGameplayClock.CurrentTime;
if (FrameStablePlayback) if (FrameStablePlayback)
// if we require frame stability, the proposed time will be adjusted to move at most one known // if we require frame stability, the proposed time will be adjusted to move at most one known
@ -149,14 +157,14 @@ namespace osu.Game.Rulesets.UI
if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime) if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime)
direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; direction = proposedTime >= manualClock.CurrentTime ? 1 : -1;
double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime); double timeBehind = Math.Abs(proposedTime - referenceClock.CurrentTime);
frameStableClock.IsCatchingUp.Value = timeBehind > 200; isCatchingUp.Value = timeBehind > 200;
frameStableClock.WaitingOnFrames.Value = state == PlaybackState.NotValid; waitingOnFrames.Value = state == PlaybackState.NotValid;
manualClock.CurrentTime = proposedTime; manualClock.CurrentTime = proposedTime;
manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; manualClock.Rate = Math.Abs(referenceClock.Rate) * direction;
manualClock.IsRunning = parentGameplayClock.IsRunning; manualClock.IsRunning = referenceClock.IsRunning;
// determine whether catch-up is required. // determine whether catch-up is required.
if (state == PlaybackState.Valid && timeBehind > 0) if (state == PlaybackState.Valid && timeBehind > 0)
@ -174,6 +182,8 @@ namespace osu.Game.Rulesets.UI
/// <returns>Whether playback is still valid.</returns> /// <returns>Whether playback is still valid.</returns>
private bool updateReplay(ref double proposedTime) private bool updateReplay(ref double proposedTime)
{ {
Debug.Assert(ReplayInputHandler != null);
double? newTime; double? newTime;
if (FrameStablePlayback) if (FrameStablePlayback)
@ -210,6 +220,8 @@ namespace osu.Game.Rulesets.UI
/// <param name="proposedTime">The time which is to be displayed.</param> /// <param name="proposedTime">The time which is to be displayed.</param>
private void applyFrameStability(ref double proposedTime) private void applyFrameStability(ref double proposedTime)
{ {
const double sixty_frame_time = 1000.0 / 60;
if (firstConsumption) if (firstConsumption)
{ {
// On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour.
@ -233,20 +245,54 @@ namespace osu.Game.Rulesets.UI
} }
} }
private void setClock() #region Delegation of IGameplayClock
public IBindable<bool> IsPaused { get; } = new BindableBool();
public double CurrentTime => framedClock.CurrentTime;
public double Rate => framedClock.Rate;
public bool IsRunning => framedClock.IsRunning;
public void ProcessFrame() { }
public double ElapsedFrameTime => framedClock.ElapsedFrameTime;
public double FramesPerSecond => framedClock.FramesPerSecond;
public FrameTimeInfo TimeInfo => framedClock.TimeInfo;
public double TrueGameplayRate
{ {
if (parentGameplayClock == null) get
{ {
// in case a parent gameplay clock isn't available, just use the parent clock. double baseRate = Rate;
parentGameplayClock ??= Clock;
} foreach (double adjustment in NonGameplayAdjustments)
else {
{ if (Precision.AlmostEquals(adjustment, 0))
Clock = frameStableClock; return 0;
baseRate /= adjustment;
}
return baseRate;
} }
} }
public ReplayInputHandler ReplayInputHandler { get; set; } public double? StartTime => parentGameplayClock?.StartTime;
public IEnumerable<double> NonGameplayAdjustments => parentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<double>();
#endregion
#region Delegation of IFrameStableClock
IBindable<bool> IFrameStableClock.IsCatchingUp => isCatchingUp;
IBindable<bool> IFrameStableClock.WaitingOnFrames => waitingOnFrames;
#endregion
private enum PlaybackState private enum PlaybackState
{ {
@ -266,25 +312,5 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
Valid Valid
} }
private class FrameStabilityClock : GameplayClock, IFrameStableClock
{
public GameplayClock ParentGameplayClock;
public readonly Bindable<bool> IsCatchingUp = new Bindable<bool>();
public readonly Bindable<bool> WaitingOnFrames = new Bindable<bool>();
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<Bindable<double>>();
public FrameStabilityClock(FramedClock underlyingClock)
: base(underlyingClock)
{
}
IBindable<bool> IFrameStableClock.IsCatchingUp => IsCatchingUp;
IBindable<bool> IFrameStableClock.WaitingOnFrames => WaitingOnFrames;
}
} }
} }

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.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Timing; using osu.Framework.Timing;

View File

@ -3,12 +3,20 @@
#nullable disable #nullable disable
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
public enum PlayfieldBorderStyle public enum PlayfieldBorderStyle
{ {
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.BorderNone))]
None, None,
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.BorderCorners))]
Corners, Corners,
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.BorderFull))]
Full Full
} }
} }

View File

@ -4,7 +4,6 @@
#nullable disable #nullable disable
using System.IO; using System.IO;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -78,7 +77,7 @@ namespace osu.Game.Screens.Edit.Setup
// remove the previous background for now. // remove the previous background for now.
// in the future we probably want to check if this is being used elsewhere (other difficulties?) // in the future we probably want to check if this is being used elsewhere (other difficulties?)
var oldFile = set.Files.FirstOrDefault(f => f.Filename == working.Value.Metadata.BackgroundFile); var oldFile = set.GetFile(working.Value.Metadata.BackgroundFile);
using (var stream = source.OpenRead()) using (var stream = source.OpenRead())
{ {
@ -107,7 +106,7 @@ namespace osu.Game.Screens.Edit.Setup
// remove the previous audio track for now. // remove the previous audio track for now.
// in the future we probably want to check if this is being used elsewhere (other difficulties?) // in the future we probably want to check if this is being used elsewhere (other difficulties?)
var oldFile = set.Files.FirstOrDefault(f => f.Filename == working.Value.Metadata.AudioFile); var oldFile = set.GetFile(working.Value.Metadata.AudioFile);
using (var stream = source.OpenRead()) using (var stream = source.OpenRead())
{ {

Some files were not shown because too many files have changed in this diff Show More