diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 29cbdd2d37..0da1f9636b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -79,9 +79,14 @@ jobs:
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
- dotnet codefilesanity | while read -r line; do
- echo "::warning::$line"
- done
+ exit_code=0
+ while read -r line; do
+ if [[ ! -z "$line" ]]; then
+ echo "::error::$line"
+ exit_code=1
+ fi
+ done <<< $(dotnet codefilesanity)
+ exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
diff --git a/osu.Android.props b/osu.Android.props
index 5a0e7479fa..956093b2ac 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
index 5580358f89..2fab47f857 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
@@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
- [TestCase(4.050601681491468d, "diffcalc-test")]
+ [TestCase(4.0505463516206195d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(5.169743871843191d, "diffcalc-test")]
+ [TestCase(5.1696411260785498d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new CatchModDoubleTime());
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index 9140e8afce..aee3268544 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -47,8 +47,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods,
- // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
+ GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate),
ScoreMultiplier = getScoreMultiplier(mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 19881b5c33..15675e74d1 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.6634445062299665d, "diffcalc-test")]
- [TestCase(1.0414203870195022d, "zero-length-sliders")]
+ [TestCase(6.5867229481955389d, "diffcalc-test")]
+ [TestCase(1.0416315570967911d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(8.3858089051603368d, "diffcalc-test")]
- [TestCase(1.2723279173428435d, "zero-length-sliders")]
+ [TestCase(8.2730989071947896d, "diffcalc-test")]
+ [TestCase(1.2726413186221039d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index 9da583a073..52ab39cfbd 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
{
+ private const double spinner_start_time = 100;
+ private const double spinner_duration = 6000;
+
[Resolved]
private AudioManager audioManager { get; set; }
@@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
- addSeekStep(5000);
+ addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
{
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
@@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
- addSeekStep(2500);
+ addSeekStep(spinner_start_time + 2500);
AddAssert("disc rotation rewound",
// 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%
@@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// 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));
- addSeekStep(5000);
+ addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
@@ -140,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinnerNormalBonusRewinding()
{
- addSeekStep(1000);
+ addSeekStep(spinner_start_time + 1000);
AddAssert("player score matching expected bonus score", () =>
{
@@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
}
- private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
- {
- Frames = scoreReplay
- .Frames
- .Cast()
- .Select(replayFrame =>
- {
- var adjustedTime = replayFrame.Time * rate;
- return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
- })
- .Cast()
- .ToList()
- };
-
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
-
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
@@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests
new Spinner
{
Position = new Vector2(256, 192),
- EndTime = 6000,
+ StartTime = spinner_start_time,
+ Duration = spinner_duration
},
}
};
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index ac77a93239..bd4c0f2ad5 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double FlashlightRating { get; set; }
public double ApproachRate { get; set; }
public double OverallDifficulty { get; set; }
+ public double DrainRate { get; set; }
public int HitCircleCount { get; set; }
public int SpinnerCount { get; set; }
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 76ea69e5c3..790aa0eb7d 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -56,7 +56,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
- double preempt = (int)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;
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
@@ -74,6 +75,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
FlashlightRating = flashlightRating,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
+ DrainRate = drainRate,
MaxCombo = maxCombo,
HitCircleCount = hitCirclesCount,
SpinnerCount = spinnerCount,
@@ -100,8 +102,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
HitWindows hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
- // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
+ hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
return new Skill[]
{
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 9d5887c26e..4e4dbc02a1 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -40,9 +40,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
- // Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
+ // Custom multipliers for NoFail and SpunOut.
if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
@@ -114,9 +114,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
- // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
- if (mods.Any(h => h is OsuModHidden))
+ if (mods.Any(m => m is OsuModBlinds))
+ aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
+ else if (mods.Any(h => h is OsuModHidden))
+ {
+ // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
+ }
aimValue *= approachRateBonus;
@@ -153,11 +157,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
- if (mods.Any(m => m is OsuModHidden))
+ if (mods.Any(m => m is OsuModBlinds))
+ {
+ // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
+ speedValue *= 1.12;
+ }
+ else if (mods.Any(m => m is OsuModHidden))
+ {
+ // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
+ }
// Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
+
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@@ -189,8 +202,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
- if (mods.Any(m => m is OsuModHidden))
+ // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
+ if (mods.Any(m => m is OsuModBlinds))
+ accuracyValue *= 1.14;
+ else if (mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08;
+
if (mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 16a18cbcb9..d8f4aa1229 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -22,17 +22,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
}
- protected override double SkillMultiplier => 26.25;
- protected override double StrainDecayBase => 0.15;
+ private double currentStrain = 1;
- protected override double StrainValueOf(DifficultyHitObject current)
+ private double skillMultiplier => 26.25;
+ private double strainDecayBase => 0.15;
+
+ private double strainValueOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
var osuCurrent = (OsuDifficultyHitObject)current;
- double result = 0;
+ double aimStrain = 0;
if (Previous.Count > 0)
{
@@ -46,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
Math.Max(osuPrevious.JumpDistance - scale, 0)
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
* Math.Max(osuCurrent.JumpDistance - scale, 0));
- result = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
+ aimStrain = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
}
}
@@ -54,11 +56,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance);
return Math.Max(
- result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
+ aimStrain + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
(Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime
);
}
private double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
+
+ private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
+
+ protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
+
+ protected override double StrainValueAt(DifficultyHitObject current)
+ {
+ currentStrain *= strainDecay(current.DeltaTime);
+ currentStrain += strainValueOf(current) * skillMultiplier;
+
+ return currentStrain;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
index abd900a80d..e3abe7d700 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
@@ -19,12 +19,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
}
- protected override double SkillMultiplier => 0.15;
- protected override double StrainDecayBase => 0.15;
+ private double skillMultiplier => 0.15;
+ private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0;
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
+ private double currentStrain = 1;
- protected override double StrainValueOf(DifficultyHitObject current)
+ private double strainValueOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
@@ -62,5 +63,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return Math.Pow(smallDistNerf * result, 2.0);
}
+
+ private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
+
+ protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
+
+ protected override double StrainValueAt(DifficultyHitObject current)
+ {
+ currentStrain *= strainDecay(current.DeltaTime);
+ currentStrain += strainValueOf(current) * skillMultiplier;
+
+ return currentStrain;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
index 7bcd867a9c..e47edc37cc 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
@@ -10,7 +10,7 @@ using osu.Framework.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
- public abstract class OsuStrainSkill : StrainDecaySkill
+ public abstract class OsuStrainSkill : StrainSkill
{
///
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 9364b11048..cae6b8e01c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -16,19 +16,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public class Speed : OsuStrainSkill
{
private const double single_spacing_threshold = 125;
-
- private const double angle_bonus_begin = 5 * Math.PI / 6;
- private const double pi_over_4 = Math.PI / 4;
- private const double pi_over_2 = Math.PI / 2;
-
- protected override double SkillMultiplier => 1400;
- protected override double StrainDecayBase => 0.3;
- protected override int ReducedSectionCount => 5;
- protected override double DifficultyMultiplier => 1.04;
-
+ private const double rhythm_multiplier = 0.75;
+ private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
private const double min_speed_bonus = 75; // ~200BPM
private const double speed_balancing_factor = 40;
+ private double skillMultiplier => 1375;
+ private double strainDecayBase => 0.3;
+
+ private double currentStrain = 1;
+ private double currentRhythm = 1;
+
+ protected override int ReducedSectionCount => 5;
+ protected override double DifficultyMultiplier => 1.04;
+ protected override int HistoryLength => 32;
+
private readonly double greatWindow;
public Speed(Mod[] mods, double hitWindowGreat)
@@ -37,52 +39,138 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
greatWindow = hitWindowGreat;
}
- protected override double StrainValueOf(DifficultyHitObject current)
+ ///
+ /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current .
+ ///
+ private double calculateRhythmBonus(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
- var osuCurrent = (OsuDifficultyHitObject)current;
- var osuPrevious = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
+ int previousIslandSize = 0;
- double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance);
- double strainTime = osuCurrent.StrainTime;
+ double rhythmComplexitySum = 0;
+ int islandSize = 1;
+ double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
+ bool firstDeltaSwitch = false;
+
+ for (int i = Previous.Count - 2; i > 0; i--)
+ {
+ OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1];
+ OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i];
+ OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1];
+
+ double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now
+
+ if (currHistoricalDecay != 0)
+ {
+ currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
+
+ double currDelta = currObj.StrainTime;
+ double prevDelta = prevObj.StrainTime;
+ double lastDelta = lastObj.StrainTime;
+ double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
+
+ double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
+
+ windowPenalty = Math.Min(1, windowPenalty);
+
+ double effectiveRatio = windowPenalty * currRatio;
+
+ if (firstDeltaSwitch)
+ {
+ if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
+ {
+ if (islandSize < 7)
+ islandSize++; // island is still progressing, count size.
+ }
+ else
+ {
+ if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
+ effectiveRatio *= 0.125;
+
+ if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
+ effectiveRatio *= 0.25;
+
+ if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
+ effectiveRatio *= 0.25;
+
+ if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
+ effectiveRatio *= 0.50;
+
+ if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
+ effectiveRatio *= 0.125;
+
+ rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
+
+ startRatio = effectiveRatio;
+
+ previousIslandSize = islandSize; // log the last island size.
+
+ if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
+ firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
+
+ islandSize = 1;
+ }
+ }
+ else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
+ {
+ // Begin counting island until we change speed again.
+ firstDeltaSwitch = true;
+ startRatio = effectiveRatio;
+ islandSize = 1;
+ }
+ }
+ }
+
+ return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
+ }
+
+ private double strainValueOf(DifficultyHitObject current)
+ {
+ if (current.BaseObject is Spinner)
+ return 0;
+
+ // derive strainTime for calculation
+ var osuCurrObj = (OsuDifficultyHitObject)current;
+ var osuPrevObj = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
+
+ double strainTime = osuCurrObj.StrainTime;
double greatWindowFull = greatWindow * 2;
double speedWindowRatio = strainTime / greatWindowFull;
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
- if (osuPrevious != null && strainTime < greatWindowFull && osuPrevious.StrainTime > strainTime)
- strainTime = Interpolation.Lerp(osuPrevious.StrainTime, strainTime, speedWindowRatio);
+ if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime)
+ strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio);
// Cap deltatime to the OD 300 hitwindow.
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
+ // derive speedBonus for calculation
double speedBonus = 1.0;
+
if (strainTime < min_speed_bonus)
- speedBonus = 1 + Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
+ speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
- double angleBonus = 1.0;
+ double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance);
- if (osuCurrent.Angle != null && osuCurrent.Angle.Value < angle_bonus_begin)
- {
- angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57;
+ return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
+ }
- if (osuCurrent.Angle.Value < pi_over_2)
- {
- angleBonus = 1.28;
- if (distance < 90 && osuCurrent.Angle.Value < pi_over_4)
- angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1);
- else if (distance < 90)
- angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - osuCurrent.Angle.Value) / pi_over_4);
- }
- }
+ private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
- return (1 + (speedBonus - 1) * 0.75)
- * angleBonus
- * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5))
- / strainTime;
+ protected override double CalculateInitialStrain(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime);
+
+ protected override double StrainValueAt(DifficultyHitObject current)
+ {
+ currentStrain *= strainDecay(current.DeltaTime);
+ currentStrain += strainValueOf(current) * skillMultiplier;
+
+ currentRhythm = calculateRhythmBonus(current);
+
+ return currentStrain * currentRhythm;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index dd3c6b317a..4b0b74ad27 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(2.2867022617692685d, "diffcalc-test")]
- [TestCase(2.2867022617692685d, "diffcalc-test-strong")]
+ [TestCase(2.2420075288523802d, "diffcalc-test")]
+ [TestCase(2.2420075288523802d, "diffcalc-test-strong")]
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(3.1704781712282624d, "diffcalc-test")]
- [TestCase(3.1704781712282624d, "diffcalc-test-strong")]
+ [TestCase(3.134084469440479d, "diffcalc-test")]
+ [TestCase(3.134084469440479d, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 4708af99e2..32aad6c36a 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -47,10 +47,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
- if (!(original.Difficulty is TaikoMutliplierAppliedDifficulty))
+ if (!(original.Difficulty is TaikoMultiplierAppliedDifficulty))
{
// Rewrite the beatmap info to add the slider velocity multiplier
- original.Difficulty = new TaikoMutliplierAppliedDifficulty(original.Difficulty);
+ original.Difficulty = new TaikoMultiplierAppliedDifficulty(original.Difficulty);
}
Beatmap converted = base.ConvertBeatmap(original, cancellationToken);
@@ -191,15 +191,15 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap CreateBeatmap() => new TaikoBeatmap();
- private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty
+ private class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
{
- public TaikoMutliplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
+ public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
{
CopyFrom(difficulty);
}
[UsedImplicitly]
- public TaikoMutliplierAppliedDifficulty()
+ public TaikoMultiplierAppliedDifficulty()
{
}
@@ -208,14 +208,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
public override void CopyTo(BeatmapDifficulty other)
{
base.CopyTo(other);
- if (!(other is TaikoMutliplierAppliedDifficulty))
+ if (!(other is TaikoMultiplierAppliedDifficulty))
SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
public override void CopyFrom(IBeatmapDifficultyInfo other)
{
base.CopyFrom(other);
- if (!(other is TaikoMutliplierAppliedDifficulty))
+ if (!(other is TaikoMultiplierAppliedDifficulty))
SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index e755bb2325..7dd47e804b 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -94,8 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
StaminaStrain = staminaRating,
RhythmStrain = rhythmRating,
ColourStrain = colourRating,
- // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate,
+ GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
Skills = skills
};
diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index 2c2c4dc24e..af87fc17ad 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -509,5 +509,17 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(LinkAction.External, result.Action);
Assert.AreEqual("/relative", result.Argument);
}
+
+ [TestCase("https://dev.ppy.sh/home/changelog", "")]
+ [TestCase("https://dev.ppy.sh/home/changelog/lazer/2021.1012", "lazer/2021.1012")]
+ public void TestChangelogLinks(string link, string expectedArg)
+ {
+ MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
+
+ LinkDetails result = MessageFormatter.GetLinkDetails(link);
+
+ Assert.AreEqual(LinkAction.OpenChangelog, result.Action);
+ Assert.AreEqual(expectedArg, result.Argument);
+ }
}
}
diff --git a/osu.Game.Tests/Database/FileStoreTests.cs b/osu.Game.Tests/Database/FileStoreTests.cs
new file mode 100644
index 0000000000..861de5303d
--- /dev/null
+++ b/osu.Game.Tests/Database/FileStoreTests.cs
@@ -0,0 +1,114 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Logging;
+using osu.Game.Models;
+using osu.Game.Stores;
+
+#nullable enable
+
+namespace osu.Game.Tests.Database
+{
+ public class FileStoreTests : RealmTest
+ {
+ [Test]
+ public void TestImportFile()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
+
+ realm.Write(() => files.Add(testData, realm));
+
+ Assert.True(files.Storage.Exists("0/05/054edec1d0211f624fed0cbca9d4f9400b0e491c43742af2c5b0abebf0c990d8"));
+ Assert.True(files.Storage.Exists(realm.All().First().StoragePath));
+ });
+ }
+
+ [Test]
+ public void TestImportSameFileTwice()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
+
+ realm.Write(() => files.Add(testData, realm));
+ realm.Write(() => files.Add(testData, realm));
+
+ Assert.AreEqual(1, realm.All().Count());
+ });
+ }
+
+ [Test]
+ public void TestDontPurgeReferenced()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
+
+ var timer = new Stopwatch();
+ timer.Start();
+
+ realm.Write(() =>
+ {
+ // attach the file to an arbitrary beatmap
+ var beatmapSet = CreateBeatmapSet(CreateRuleset());
+
+ beatmapSet.Files.Add(new RealmNamedFileUsage(file, "arbitrary.resource"));
+
+ realm.Add(beatmapSet);
+ });
+
+ Logger.Log($"Import complete at {timer.ElapsedMilliseconds}");
+
+ string path = file.StoragePath;
+
+ Assert.True(realm.All().Any());
+ Assert.True(files.Storage.Exists(path));
+
+ files.Cleanup();
+ Logger.Log($"Cleanup complete at {timer.ElapsedMilliseconds}");
+
+ Assert.True(realm.All().Any());
+ Assert.True(file.IsValid);
+ Assert.True(files.Storage.Exists(path));
+ });
+ }
+
+ [Test]
+ public void TestPurgeUnreferenced()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
+
+ string path = file.StoragePath;
+
+ Assert.True(realm.All().Any());
+ Assert.True(files.Storage.Exists(path));
+
+ files.Cleanup();
+
+ Assert.False(realm.All().Any());
+ Assert.False(file.IsValid);
+ Assert.False(files.Storage.Exists(path));
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs
index 245981cd9b..3e8b6091fd 100644
--- a/osu.Game.Tests/Database/GeneralUsageTests.cs
+++ b/osu.Game.Tests/Database/GeneralUsageTests.cs
@@ -1,3 +1,6 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
using System;
using System.Threading;
using System.Threading.Tasks;
diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs
new file mode 100644
index 0000000000..33aa1afb89
--- /dev/null
+++ b/osu.Game.Tests/Database/RealmLiveTests.cs
@@ -0,0 +1,213 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Models;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Tests.Database
+{
+ public class RealmLiveTests : RealmTest
+ {
+ [Test]
+ public void TestLiveCastability()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
+
+ ILive iBeatmap = beatmap;
+
+ Assert.AreEqual(0, iBeatmap.Value.Length);
+ });
+ }
+
+ [Test]
+ public void TestValueAccessWithOpenContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ using (realmFactory.CreateContext())
+ {
+ var resolved = liveBeatmap.Value;
+
+ Assert.IsTrue(resolved.Realm.IsClosed);
+ Assert.IsTrue(resolved.IsValid);
+
+ // can access properties without a crash.
+ Assert.IsFalse(resolved.Hidden);
+ }
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestScopedReadWithoutContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ liveBeatmap.PerformRead(beatmap =>
+ {
+ Assert.IsTrue(beatmap.IsValid);
+ Assert.IsFalse(beatmap.Hidden);
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestScopedWriteWithoutContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; });
+ liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestValueAccessWithoutOpenContextFails()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ Assert.Throws(() =>
+ {
+ var unused = liveBeatmap.Value;
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestLiveAssumptions()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ int changesTriggered = 0;
+
+ using (var updateThreadContext = realmFactory.CreateContext())
+ {
+ updateThreadContext.All().SubscribeForNotifications(gotChange);
+ RealmLive? liveBeatmap = null;
+
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var ruleset = CreateRuleset();
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ // add a second beatmap to ensure that a full refresh occurs below.
+ // not just a refresh from the resolved Live.
+ threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ // not yet seen by main context
+ Assert.AreEqual(0, updateThreadContext.All().Count());
+ Assert.AreEqual(0, changesTriggered);
+
+ var resolved = liveBeatmap.Value;
+
+ // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
+ Assert.AreEqual(2, updateThreadContext.All().Count());
+ Assert.AreEqual(1, changesTriggered);
+
+ // even though the realm that this instance was resolved for was closed, it's still valid.
+ Assert.IsTrue(resolved.Realm.IsClosed);
+ Assert.IsTrue(resolved.IsValid);
+
+ // can access properties without a crash.
+ Assert.IsFalse(resolved.Hidden);
+
+ updateThreadContext.Write(r =>
+ {
+ // can use with the main context.
+ r.Remove(resolved);
+ });
+ }
+
+ void gotChange(IRealmCollection sender, ChangeSet changes, Exception error)
+ {
+ changesTriggered++;
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs
index 576f901c1a..04c9f2577a 100644
--- a/osu.Game.Tests/Database/RealmTest.cs
+++ b/osu.Game.Tests/Database/RealmTest.cs
@@ -4,12 +4,13 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
-using Nito.AsyncEx;
using NUnit.Framework;
+using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Database;
+using osu.Game.Models;
#nullable enable
@@ -28,42 +29,109 @@ namespace osu.Game.Tests.Database
protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "")
{
- AsyncContext.Run(() =>
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{
- var testStorage = storage.GetStorageForDirectory(caller);
-
- using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ host.Run(new RealmTestGame(() =>
{
- Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
- testAction(realmFactory, testStorage);
+ var testStorage = storage.GetStorageForDirectory(caller);
- realmFactory.Dispose();
+ using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ {
+ Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
+ testAction(realmFactory, testStorage);
- Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
- realmFactory.Compact();
- Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
- }
- });
+ realmFactory.Dispose();
+
+ Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
+ realmFactory.Compact();
+ Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
+ }
+ }));
+ }
}
protected void RunTestWithRealmAsync(Func testAction, [CallerMemberName] string caller = "")
{
- AsyncContext.Run(async () =>
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{
- var testStorage = storage.GetStorageForDirectory(caller);
-
- using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ host.Run(new RealmTestGame(async () =>
{
- Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
- await testAction(realmFactory, testStorage);
+ var testStorage = storage.GetStorageForDirectory(caller);
- realmFactory.Dispose();
+ using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ {
+ Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
+ await testAction(realmFactory, testStorage);
- Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
- realmFactory.Compact();
- Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
+ realmFactory.Dispose();
+
+ Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
+ realmFactory.Compact();
+ }
+ }));
+ }
+ }
+
+ protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset)
+ {
+ RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() };
+
+ var metadata = new RealmBeatmapMetadata
+ {
+ Title = "My Love",
+ Artist = "Kuba Oms"
+ };
+
+ var beatmapSet = new RealmBeatmapSet
+ {
+ Beatmaps =
+ {
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", }
+ },
+ Files =
+ {
+ new RealmNamedFileUsage(createRealmFile(), "test [easy].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [normal].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [hard].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [insane].osu"),
}
- });
+ };
+
+ for (int i = 0; i < 8; i++)
+ beatmapSet.Files.Add(new RealmNamedFileUsage(createRealmFile(), $"hitsound{i}.mp3"));
+
+ foreach (var b in beatmapSet.Beatmaps)
+ b.BeatmapSet = beatmapSet;
+
+ return beatmapSet;
+ }
+
+ protected static RealmRuleset CreateRuleset() =>
+ new RealmRuleset(0, "osu!", "osu", true);
+
+ private class RealmTestGame : Framework.Game
+ {
+ public RealmTestGame(Func work)
+ {
+ // ReSharper disable once AsyncVoidLambda
+ Scheduler.Add(async () =>
+ {
+ await work().ConfigureAwait(true);
+ Exit();
+ });
+ }
+
+ public RealmTestGame(Action work)
+ {
+ Scheduler.Add(() =>
+ {
+ work();
+ Exit();
+ });
+ }
}
private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory)
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index 79767bc671..558b874234 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -168,14 +168,14 @@ namespace osu.Game.Tests.Online
return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host);
}
- protected override BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host)
+ protected override BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager manager, IAPIProvider api, GameHost host)
{
- return new TestBeatmapModelDownloader(modelManager, api, host);
+ return new TestBeatmapModelDownloader(manager, api, host);
}
internal class TestBeatmapModelDownloader : BeatmapModelDownloader
{
- public TestBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost)
+ public TestBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost)
: base(modelManager, apiProvider, gameHost)
{
}
diff --git a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
index ab47067411..ffb3d41d18 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
@@ -6,7 +6,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
@@ -65,10 +64,9 @@ namespace osu.Game.Tests.Skins
public new void TriggerSourceChanged() => base.TriggerSourceChanged();
- protected override void OnSourceChanged()
+ protected override void RefreshSources()
{
- ResetSources();
- sources.ForEach(AddSource);
+ SetSources(sources);
}
}
diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
index 211543a881..0107632f6e 100644
--- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
+++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -18,16 +19,19 @@ namespace osu.Game.Tests.Visual.Audio
{
public class TestSceneAudioFilter : OsuTestScene
{
- private OsuSpriteText lowpassText;
- private AudioFilter lowpassFilter;
+ private OsuSpriteText lowPassText;
+ private AudioFilter lowPassFilter;
- private OsuSpriteText highpassText;
- private AudioFilter highpassFilter;
+ private OsuSpriteText highPassText;
+ private AudioFilter highPassFilter;
private Track track;
private WaveformTestBeatmap beatmap;
+ private OsuSliderBar lowPassSlider;
+ private OsuSliderBar highPassSlider;
+
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
@@ -38,53 +42,89 @@ namespace osu.Game.Tests.Visual.Audio
{
Children = new Drawable[]
{
- lowpassFilter = new AudioFilter(audio.TrackMixer),
- highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
- lowpassText = new OsuSpriteText
+ lowPassFilter = new AudioFilter(audio.TrackMixer),
+ highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
+ lowPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
- Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz",
+ Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
- new OsuSliderBar
+ lowPassSlider = new OsuSliderBar
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
- Current = { BindTarget = lowpassFilter.Cutoff }
+ Current = new BindableInt
+ {
+ MinValue = 0,
+ MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
+ }
},
- highpassText = new OsuSpriteText
+ highPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
- Text = $"High Pass: {highpassFilter.Cutoff.Value}hz",
+ Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
- new OsuSliderBar
+ highPassSlider = new OsuSliderBar
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
- Current = { BindTarget = highpassFilter.Cutoff }
+ Current = new BindableInt
+ {
+ MinValue = 0,
+ MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
+ }
}
}
});
- lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz";
- highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz";
+
+ lowPassSlider.Current.ValueChanged += e =>
+ {
+ lowPassText.Text = $"Low Pass: {e.NewValue}hz";
+ lowPassFilter.Cutoff = e.NewValue;
+ };
+
+ highPassSlider.Current.ValueChanged += e =>
+ {
+ highPassText.Text = $"High Pass: {e.NewValue}hz";
+ highPassFilter.Cutoff = e.NewValue;
+ };
}
+ #region Overrides of Drawable
+
+ protected override void Update()
+ {
+ base.Update();
+ highPassSlider.Current.Value = highPassFilter.Cutoff;
+ lowPassSlider.Current.Value = lowPassFilter.Cutoff;
+ }
+
+ #endregion
+
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Play Track", () => track.Start());
+
+ AddStep("Reset filters", () =>
+ {
+ lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF;
+ highPassFilter.Cutoff = 0;
+ });
+
waitTrackPlay();
}
[Test]
- public void TestLowPass()
+ public void TestLowPassSweep()
{
AddStep("Filter Sweep", () =>
{
- lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
@@ -92,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
- lowpassFilter.CutoffTo(0).Then()
+ lowPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@@ -101,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio
}
[Test]
- public void TestHighPass()
+ public void TestHighPassSweep()
{
AddStep("Filter Sweep", () =>
{
- highpassFilter.CutoffTo(0).Then()
+ highPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@@ -113,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
- highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
+ highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
index 2258a209e2..f0aa3e2350 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -32,6 +32,8 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for editor load", () => editor != null);
+ AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7);
+
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("Enter compose mode", () => InputManager.Key(Key.F1));
@@ -41,11 +43,11 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left));
- AddStep("Save and exit", () =>
- {
- InputManager.Keys(PlatformAction.Save);
- InputManager.Key(Key.Escape);
- });
+ AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
+
+ AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
+
+ AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@@ -57,6 +59,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Wait for editor load", () => editor != null);
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
+ AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7);
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
index 5eb71e92c2..ae0decaee1 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
@@ -103,6 +103,30 @@ namespace osu.Game.Tests.Visual.Gameplay
checkFrameCount(0);
}
+ [Test]
+ public void TestRatePreservedWhenTimeNotProgressing()
+ {
+ AddStep("set manual clock rate", () => manualClock.Rate = 1);
+ seekManualTo(5000);
+ createStabilityContainer();
+ checkRate(1);
+
+ seekManualTo(10000);
+ checkRate(1);
+
+ AddWaitStep("wait some", 3);
+ checkRate(1);
+
+ seekManualTo(5000);
+ checkRate(-1);
+
+ AddWaitStep("wait some", 3);
+ checkRate(-1);
+
+ seekManualTo(10000);
+ checkRate(1);
+ }
+
private const int max_frames_catchup = 50;
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () =>
@@ -116,6 +140,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private void checkFrameCount(int frames) =>
AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames);
+ private void checkRate(double rate) =>
+ AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate);
+
public class ClockConsumingChild : CompositeDrawable
{
private readonly OsuSpriteText text;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
index 3ed274690e..48a97d54f7 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
@@ -90,8 +90,12 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest(() =>
{
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true);
- AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300);
+
+ // Fail occurs at 164ms with the provided beatmap.
+ // Fail animation runs for 2.5s realtime but the gameplay time change is *variable* due to the frequency transform being applied, so we need a bit of lenience.
+ AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600);
});
+
AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
new file mode 100644
index 0000000000..cb7c334656
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Database;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+ public class TestSceneStartupImport : OsuGameTestScene
+ {
+ private string importFilename;
+
+ protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { importFilename });
+
+ public override void SetUpSteps()
+ {
+ AddStep("Prepare import beatmap", () => importFilename = TestResources.GetTestBeatmapForImport());
+
+ base.SetUpSteps();
+ }
+
+ [Test]
+ public void TestImportCreatedNotification()
+ {
+ AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType().Count() == 1);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index 4bcc887b9f..d948aebbbf 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -32,12 +32,14 @@ namespace osu.Game.Tests.Visual.Playlists
private TestResultsScreen resultsScreen;
private int currentScoreId;
private bool requestComplete;
+ private int totalCount;
[SetUp]
public void Setup() => Schedule(() =>
{
currentScoreId = 0;
requestComplete = false;
+ totalCount = 0;
bindHandler();
});
@@ -53,7 +55,6 @@ namespace osu.Game.Tests.Visual.Playlists
});
createResults(() => userScore);
- waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
}
@@ -62,7 +63,6 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestShowNullUserScore()
{
createResults();
- waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
@@ -79,7 +79,6 @@ namespace osu.Game.Tests.Visual.Playlists
});
createResults(() => userScore);
- waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1);
AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
@@ -91,7 +90,6 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind delayed handler", () => bindHandler(true));
createResults();
- waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
@@ -100,7 +98,6 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestFetchWhenScrolledToTheRight()
{
createResults();
- waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true));
@@ -131,7 +128,6 @@ namespace osu.Game.Tests.Visual.Playlists
});
createResults(() => userScore);
- waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true));
@@ -161,13 +157,15 @@ namespace osu.Game.Tests.Visual.Playlists
}));
});
- AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true);
+ waitForDisplay();
}
private void waitForDisplay()
{
- AddUntilStep("wait for request to complete", () => requestComplete);
- AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true);
+ AddUntilStep("wait for load to complete", () =>
+ requestComplete
+ && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
+ && resultsScreen.ScorePanelList.AllPanelsVisible);
AddWaitStep("wait for display", 5);
}
@@ -203,6 +201,7 @@ namespace osu.Game.Tests.Visual.Playlists
triggerFail(s);
else
triggerSuccess(s, createUserResponse(userScore));
+
break;
case IndexPlaylistScoresRequest i:
@@ -248,6 +247,8 @@ namespace osu.Game.Tests.Visual.Playlists
}
};
+ totalCount++;
+
for (int i = 1; i <= scores_per_result; i++)
{
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
@@ -285,6 +286,8 @@ namespace osu.Game.Tests.Visual.Playlists
},
Statistics = userScore.Statistics
});
+
+ totalCount += 2;
}
addCursor(multiplayerUserScore.ScoresAround.Lower);
@@ -325,6 +328,8 @@ namespace osu.Game.Tests.Visual.Playlists
{ HitResult.Great, 300 }
}
});
+
+ totalCount++;
}
addCursor(result);
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
index e3dae9c27e..d530e1f796 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
@@ -53,7 +53,11 @@ namespace osu.Game.Tests.Visual.Settings
};
[SettingSource("Sample string", "Change something for a mod")]
- public Bindable StringBindable { get; } = new Bindable();
+ public Bindable StringBindable { get; } = new Bindable
+ {
+ Default = string.Empty,
+ Value = "Sample text"
+ };
}
private enum TestEnum
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index 997eac709d..dc5b0e0d77 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -3,6 +3,7 @@
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet;
@@ -21,6 +22,9 @@ namespace osu.Game.Tests.Visual.Settings
private TestTabletHandler tabletHandler;
private TabletSettings settings;
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
[SetUpSteps]
public void SetUpSteps()
{
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 067f1cabb4..4811fc979e 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -142,6 +142,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("store selected beatmap", () => selected = Beatmap.Value);
+ AddUntilStep("wait for beatmaps to load", () => songSelect.Carousel.ChildrenOfType().Any());
+
AddStep("select next and enter", () =>
{
InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType()
@@ -599,10 +601,10 @@ namespace osu.Game.Tests.Visual.SongSelect
});
FilterableDifficultyIcon difficultyIcon = null;
- AddStep("Find an icon", () =>
+ AddUntilStep("Find an icon", () =>
{
- difficultyIcon = set.ChildrenOfType()
- .First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
+ return (difficultyIcon = set.ChildrenOfType()
+ .FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex())) != null;
});
AddStep("Click on a difficulty", () =>
@@ -765,10 +767,10 @@ namespace osu.Game.Tests.Visual.SongSelect
});
FilterableGroupedDifficultyIcon groupIcon = null;
- AddStep("Find group icon for different ruleset", () =>
+ AddUntilStep("Find group icon for different ruleset", () =>
{
- groupIcon = set.ChildrenOfType()
- .First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3);
+ return (groupIcon = set.ChildrenOfType()
+ .FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3)) != null;
});
AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index 189b143a35..9a75d3c309 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -163,7 +163,6 @@ namespace osu.Game.Tests.Visual.UserInterface
});
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
-
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
}
@@ -171,6 +170,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestDeleteViaDatabase()
{
AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
+ AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
index 546e905ded..8d1572769f 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
@@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod));
- var mutlipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
- AddStep(@"Add multiple Mods", () => changeMods(mutlipleIncrementMods));
- AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(mutlipleIncrementMods));
+ var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
+ AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
+ AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods));
}
[Test]
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
new file mode 100644
index 0000000000..9ccfba7c74
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.UserInterfaceV2;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneRoundedButton : OsuTestScene
+ {
+ [Test]
+ public void TestBasic()
+ {
+ RoundedButton button = null;
+
+ AddStep("create button", () => Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.DarkGray
+ },
+ button = new RoundedButton
+ {
+ Width = 400,
+ Text = "Test button",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Action = () => { }
+ }
+ }
+ });
+
+ AddToggleStep("toggle disabled", disabled => button.Action = disabled ? (Action)null : () => { });
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs
index 5782301a65..2237e389d7 100644
--- a/osu.Game.Tournament/Components/DateTextBox.cs
+++ b/osu.Game.Tournament/Components/DateTextBox.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Components
public DateTextBox()
{
- base.Current = new Bindable();
+ base.Current = new Bindable(string.Empty);
((OsuTextBox)Control).OnCommit += (sender, newText) =>
{
diff --git a/osu.Game.Tournament/Models/SeedingResult.cs b/osu.Game.Tournament/Models/SeedingResult.cs
index 87aaf8bf36..d37c967762 100644
--- a/osu.Game.Tournament/Models/SeedingResult.cs
+++ b/osu.Game.Tournament/Models/SeedingResult.cs
@@ -10,7 +10,7 @@ namespace osu.Game.Tournament.Models
{
public List Beatmaps = new List();
- public Bindable Mod = new Bindable();
+ public Bindable Mod = new Bindable(string.Empty);
public Bindable Seed = new BindableInt
{
diff --git a/osu.Game.Tournament/Models/TournamentRound.cs b/osu.Game.Tournament/Models/TournamentRound.cs
index 08b3143be1..ab39605d07 100644
--- a/osu.Game.Tournament/Models/TournamentRound.cs
+++ b/osu.Game.Tournament/Models/TournamentRound.cs
@@ -14,8 +14,8 @@ namespace osu.Game.Tournament.Models
[Serializable]
public class TournamentRound
{
- public readonly Bindable Name = new Bindable();
- public readonly Bindable Description = new Bindable();
+ public readonly Bindable Name = new Bindable(string.Empty);
+ public readonly Bindable Description = new Bindable(string.Empty);
public readonly BindableInt BestOf = new BindableInt(9) { Default = 9, MinValue = 3, MaxValue = 23 };
diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
index 6e4fc8fe1a..1d8c4e7476 100644
--- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
@@ -149,7 +149,7 @@ namespace osu.Game.Tournament.Screens.Editors
private readonly Bindable beatmapId = new Bindable();
- private readonly Bindable mods = new Bindable();
+ private readonly Bindable mods = new Bindable(string.Empty);
private readonly Container drawableContainer;
diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
index b64a3993e6..d5b55823a5 100644
--- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
@@ -149,7 +149,7 @@ namespace osu.Game.Tournament.Screens.Editors
private readonly Bindable beatmapId = new Bindable();
- private readonly Bindable score = new Bindable();
+ private readonly Bindable score = new Bindable(string.Empty);
private readonly Container drawableContainer;
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
index 994dee4da0..77101e4023 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
@@ -127,7 +127,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth);
}
- private class MatchScoreCounter : ScoreCounter
+ private class MatchScoreCounter : CommaSeparatedScoreCounter
{
private OsuSpriteText displayedSpriteText;
diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs
index ee48bdd7d9..d2a39e9db7 100644
--- a/osu.Game/Audio/Effects/AudioFilter.cs
+++ b/osu.Game/Audio/Effects/AudioFilter.cs
@@ -4,7 +4,6 @@
using System.Diagnostics;
using ManagedBass.Fx;
using osu.Framework.Audio.Mixing;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Audio.Effects
@@ -21,10 +20,25 @@ namespace osu.Game.Audio.Effects
private readonly BQFParameters filter;
private readonly BQFType type;
+ private bool isAttached;
+
+ private int cutoff;
+
///
- /// The current cutoff of this filter.
+ /// The cutoff frequency of this filter.
///
- public BindableNumber Cutoff { get; }
+ public int Cutoff
+ {
+ get => cutoff;
+ set
+ {
+ if (value == cutoff)
+ return;
+
+ cutoff = value;
+ updateFilter(cutoff);
+ }
+ }
///
/// A Component that implements a BASS FX BiQuad Filter Effect.
@@ -36,102 +50,96 @@ namespace osu.Game.Audio.Effects
this.mixer = mixer;
this.type = type;
- int initialCutoff;
-
- switch (type)
- {
- case BQFType.HighPass:
- initialCutoff = 1;
- break;
-
- case BQFType.LowPass:
- initialCutoff = MAX_LOWPASS_CUTOFF;
- break;
-
- default:
- initialCutoff = 500; // A default that should ensure audio remains audible for other filters.
- break;
- }
-
- Cutoff = new BindableNumber(initialCutoff)
- {
- MinValue = 1,
- MaxValue = MAX_LOWPASS_CUTOFF
- };
-
filter = new BQFParameters
{
lFilter = type,
- fCenter = initialCutoff,
fBandwidth = 0,
- fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
+ // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
+ fQ = 0.7f
};
- // Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic)
- if (type != BQFType.LowPass && type != BQFType.HighPass)
- attachFilter();
-
- Cutoff.ValueChanged += updateFilter;
+ Cutoff = getInitialCutoff(type);
}
- private void attachFilter()
+ private int getInitialCutoff(BQFType type)
{
- Debug.Assert(!mixer.Effects.Contains(filter));
- mixer.Effects.Add(filter);
- }
-
- private void detachFilter()
- {
- Debug.Assert(mixer.Effects.Contains(filter));
- mixer.Effects.Remove(filter);
- }
-
- private void updateFilter(ValueChangedEvent cutoff)
- {
- // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
- if (type == BQFType.LowPass)
+ switch (type)
{
- if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF)
- {
- detachFilter();
- return;
- }
+ case BQFType.HighPass:
+ return 1;
- if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF)
- attachFilter();
+ case BQFType.LowPass:
+ return MAX_LOWPASS_CUTOFF;
+
+ default:
+ return 500; // A default that should ensure audio remains audible for other filters.
+ }
+ }
+
+ private void updateFilter(int newValue)
+ {
+ switch (type)
+ {
+ case BQFType.LowPass:
+ // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
+ if (newValue >= MAX_LOWPASS_CUTOFF)
+ {
+ ensureDetached();
+ return;
+ }
+
+ break;
+
+ // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
+ case BQFType.HighPass:
+ if (newValue <= 1)
+ {
+ ensureDetached();
+ return;
+ }
+
+ break;
}
- // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
- if (type == BQFType.HighPass)
- {
- if (cutoff.NewValue <= 1)
- {
- detachFilter();
- return;
- }
-
- if (cutoff.OldValue <= 1 && cutoff.NewValue > 1)
- attachFilter();
- }
+ ensureAttached();
var filterIndex = mixer.Effects.IndexOf(filter);
+
if (filterIndex < 0) return;
if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
{
- existingFilter.fCenter = cutoff.NewValue;
+ existingFilter.fCenter = newValue;
// required to update effect with new parameters.
mixer.Effects[filterIndex] = existingFilter;
}
}
+ private void ensureAttached()
+ {
+ if (isAttached)
+ return;
+
+ Debug.Assert(!mixer.Effects.Contains(filter));
+ mixer.Effects.Add(filter);
+ isAttached = true;
+ }
+
+ private void ensureDetached()
+ {
+ if (!isAttached)
+ return;
+
+ Debug.Assert(mixer.Effects.Contains(filter));
+ mixer.Effects.Remove(filter);
+ isAttached = false;
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
-
- if (mixer.Effects.Contains(filter))
- detachFilter();
+ ensureDetached();
}
}
}
diff --git a/osu.Game/Audio/Effects/ITransformableFilter.cs b/osu.Game/Audio/Effects/ITransformableFilter.cs
index e4de4cf8ff..fb6a924f68 100644
--- a/osu.Game/Audio/Effects/ITransformableFilter.cs
+++ b/osu.Game/Audio/Effects/ITransformableFilter.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
@@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects
///
/// The filter cutoff.
///
- BindableNumber Cutoff { get; }
+ int Cutoff { get; set; }
}
public static class FilterableAudioComponentExtensions
@@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects
public static TransformSequence CutoffTo(this T component, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
- => component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing);
+ => component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing);
///
/// Smoothly adjusts filter cutoff over time.
@@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects
public static TransformSequence CutoffTo(this TransformSequence sequence, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
- => sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing));
+ => sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing));
}
}
diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs
index f3434c5153..627e54c803 100644
--- a/osu.Game/Beatmaps/BeatmapConverter.cs
+++ b/osu.Game/Beatmaps/BeatmapConverter.cs
@@ -40,7 +40,13 @@ namespace osu.Game.Beatmaps
public IBeatmap Convert(CancellationToken cancellationToken = default)
{
// We always operate on a clone of the original beatmap, to not modify it game-wide
- return ConvertBeatmap(Beatmap.Clone(), cancellationToken);
+ var original = Beatmap.Clone();
+
+ // Shallow clone isn't enough to ensure we don't mutate beatmap info unexpectedly.
+ // Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`.
+ original.BeatmapInfo = original.BeatmapInfo.Clone();
+
+ return ConvertBeatmap(original, cancellationToken);
}
///
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 240db22c00..14175f251b 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps
}
}
- protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host)
+ protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider api, GameHost host)
{
return new BeatmapModelDownloader(modelManager, api, host);
}
diff --git a/osu.Game/Beatmaps/BeatmapModelDownloader.cs b/osu.Game/Beatmaps/BeatmapModelDownloader.cs
index ae482eeafd..30dc95a966 100644
--- a/osu.Game/Beatmaps/BeatmapModelDownloader.cs
+++ b/osu.Game/Beatmaps/BeatmapModelDownloader.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps
protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
- public BeatmapModelDownloader(BeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null)
+ public BeatmapModelDownloader(IBeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null)
: base(beatmapModelManager, api, host)
{
}
diff --git a/osu.Game/Configuration/RandomSelectAlgorithm.cs b/osu.Game/Configuration/RandomSelectAlgorithm.cs
index 8d0c87374f..b22f2ae485 100644
--- a/osu.Game/Configuration/RandomSelectAlgorithm.cs
+++ b/osu.Game/Configuration/RandomSelectAlgorithm.cs
@@ -10,7 +10,7 @@ namespace osu.Game.Configuration
[Description("Never repeat")]
RandomPermutation,
- [Description("Random")]
+ [Description("True Random")]
Random
}
}
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index ee1a7e2900..c235fc7728 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -116,7 +116,7 @@ namespace osu.Game.Database
/// One or more archive locations on disk.
public Task Import(params string[] paths)
{
- var notification = new ProgressNotification { State = ProgressNotificationState.Active };
+ var notification = new ImportProgressNotification();
PostNotification?.Invoke(notification);
@@ -125,7 +125,7 @@ namespace osu.Game.Database
public Task Import(params ImportTask[] tasks)
{
- var notification = new ProgressNotification { State = ProgressNotificationState.Active };
+ var notification = new ImportProgressNotification();
PostNotification?.Invoke(notification);
diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs
new file mode 100644
index 0000000000..024d9f2a89
--- /dev/null
+++ b/osu.Game/Database/IHasRealmFiles.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Models;
+
+#nullable enable
+
+namespace osu.Game.Database
+{
+ ///
+ /// A model that contains a list of files it is responsible for.
+ ///
+ public interface IHasRealmFiles
+ {
+ IList Files { get; }
+
+ string Hash { get; set; }
+ }
+}
diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs
index e94af01772..8e658cb0f5 100644
--- a/osu.Game/Database/IModelImporter.cs
+++ b/osu.Game/Database/IModelImporter.cs
@@ -10,7 +10,7 @@ using osu.Game.Overlays.Notifications;
namespace osu.Game.Database
{
///
- /// A class which handles importing of asociated models to the game store.
+ /// A class which handles importing of associated models to the game store.
///
/// The model type.
public interface IModelImporter : IPostNotifications
diff --git a/osu.Game/Database/INamedFile.cs b/osu.Game/Database/INamedFile.cs
new file mode 100644
index 0000000000..2bd45d4e42
--- /dev/null
+++ b/osu.Game/Database/INamedFile.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Models;
+
+#nullable enable
+
+namespace osu.Game.Database
+{
+ ///
+ /// Represents a join model which gives a filename and scope to a .
+ ///
+ public interface INamedFile
+ {
+ string Filename { get; set; }
+
+ RealmFile File { get; set; }
+ }
+}
diff --git a/osu.Game/Database/ImportProgressNotification.cs b/osu.Game/Database/ImportProgressNotification.cs
new file mode 100644
index 0000000000..aaee3e117f
--- /dev/null
+++ b/osu.Game/Database/ImportProgressNotification.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Overlays.Notifications;
+
+namespace osu.Game.Database
+{
+ public class ImportProgressNotification : ProgressNotification
+ {
+ public ImportProgressNotification()
+ {
+ State = ProgressNotificationState.Active;
+ }
+ }
+}
diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs
index 0ff902a8bc..c3810eb441 100644
--- a/osu.Game/Database/RealmContextFactory.cs
+++ b/osu.Game/Database/RealmContextFactory.cs
@@ -135,9 +135,8 @@ namespace osu.Game.Database
if (IsDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
- // TODO: this can be added for safety once we figure how to bypass in test
- // if (!ThreadSafety.IsUpdateThread)
- // throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread.");
+ if (!ThreadSafety.IsUpdateThread)
+ throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread.");
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs
new file mode 100644
index 0000000000..abb69644d6
--- /dev/null
+++ b/osu.Game/Database/RealmLive.cs
@@ -0,0 +1,111 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Threading;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Database
+{
+ ///
+ /// Provides a method of working with realm objects over longer application lifetimes.
+ ///
+ /// The underlying object type.
+ public class RealmLive : ILive where T : RealmObject, IHasGuidPrimaryKey
+ {
+ public Guid ID { get; }
+
+ private readonly SynchronizationContext? fetchedContext;
+ private readonly int fetchedThreadId;
+
+ ///
+ /// The original live data used to create this instance.
+ ///
+ private readonly T data;
+
+ ///
+ /// Construct a new instance of live realm data.
+ ///
+ /// The realm data.
+ public RealmLive(T data)
+ {
+ this.data = data;
+
+ fetchedContext = SynchronizationContext.Current;
+ fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
+
+ ID = data.ID;
+ }
+
+ ///
+ /// Perform a read operation on this live object.
+ ///
+ /// The action to perform.
+ public void PerformRead(Action perform)
+ {
+ if (originalDataValid)
+ {
+ perform(data);
+ return;
+ }
+
+ using (var realm = Realm.GetInstance(data.Realm.Config))
+ perform(realm.Find(ID));
+ }
+
+ ///
+ /// Perform a read operation on this live object.
+ ///
+ /// The action to perform.
+ public TReturn PerformRead(Func perform)
+ {
+ if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
+ throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
+
+ if (originalDataValid)
+ return perform(data);
+
+ using (var realm = Realm.GetInstance(data.Realm.Config))
+ return perform(realm.Find(ID));
+ }
+
+ ///
+ /// Perform a write operation on this live object.
+ ///
+ /// The action to perform.
+ public void PerformWrite(Action perform) =>
+ PerformRead(t =>
+ {
+ var transaction = t.Realm.BeginWrite();
+ perform(t);
+ transaction.Commit();
+ });
+
+ public T Value
+ {
+ get
+ {
+ if (originalDataValid)
+ return data;
+
+ T retrieved;
+
+ using (var realm = Realm.GetInstance(data.Realm.Config))
+ retrieved = realm.Find(ID);
+
+ if (!retrieved.IsValid)
+ throw new InvalidOperationException("Attempted to access value without an open context");
+
+ return retrieved;
+ }
+ }
+
+ private bool originalDataValid => isCorrectThread && data.IsValid;
+
+ // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72)
+ private bool isCorrectThread
+ => (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId;
+ }
+}
diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs
index c5aa1399a3..18a926fa8c 100644
--- a/osu.Game/Database/RealmObjectExtensions.cs
+++ b/osu.Game/Database/RealmObjectExtensions.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using AutoMapper;
using osu.Game.Input.Bindings;
using Realms;
@@ -47,5 +48,17 @@ namespace osu.Game.Database
return mapper.Map(item);
}
+
+ public static List> ToLive(this IEnumerable realmList)
+ where T : RealmObject, IHasGuidPrimaryKey
+ {
+ return realmList.Select(l => new RealmLive(l)).ToList();
+ }
+
+ public static RealmLive ToLive(this T realmObject)
+ where T : RealmObject, IHasGuidPrimaryKey
+ {
+ return new RealmLive(realmObject);
+ }
}
}
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index d7cfc4094c..af2bb26871 100644
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -225,6 +225,16 @@ namespace osu.Game.Graphics
public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee");
public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff");
+ ///
+ /// Equivalent to 's .
+ ///
+ public readonly Color4 Pink3 = Color4Extensions.FromHex(@"cc3378");
+
+ ///
+ /// Equivalent to 's .
+ ///
+ public readonly Color4 Blue3 = Color4Extensions.FromHex(@"3399cc");
+
///
/// Equivalent to 's .
///
diff --git a/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs
new file mode 100644
index 0000000000..4e1c612f09
--- /dev/null
+++ b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Extensions.LocalisationExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Graphics.UserInterface
+{
+ public abstract class CommaSeparatedScoreCounter : RollingCounter
+ {
+ protected override double RollingDuration => 1000;
+ protected override Easing RollingEasing => Easing.Out;
+
+ protected override double GetProportionalDuration(double currentValue, double newValue) =>
+ currentValue > newValue ? currentValue - newValue : newValue - currentValue;
+
+ protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(@"N0");
+
+ protected override OsuSpriteText CreateSpriteText()
+ => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true));
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
index 7ebf3819e4..25f19aa0a9 100644
--- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs
+++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
@@ -13,43 +14,30 @@ namespace osu.Game.Graphics.UserInterface
protected override double RollingDuration => 1000;
protected override Easing RollingEasing => Easing.Out;
- ///
- /// Whether comma separators should be displayed.
- ///
- public bool UseCommaSeparator { get; }
-
public Bindable RequiredDisplayDigits { get; } = new Bindable();
+ private string formatString;
+
///
/// Displays score.
///
/// How many leading zeroes the counter will have.
- /// Whether comma separators should be displayed.
- protected ScoreCounter(int leading = 0, bool useCommaSeparator = false)
+ protected ScoreCounter(int leading = 0)
{
- UseCommaSeparator = useCommaSeparator;
-
RequiredDisplayDigits.Value = leading;
- RequiredDisplayDigits.BindValueChanged(_ => UpdateDisplay());
+ RequiredDisplayDigits.BindValueChanged(displayDigitsChanged, true);
}
- protected override double GetProportionalDuration(double currentValue, double newValue)
+ private void displayDigitsChanged(ValueChangedEvent _)
{
- return currentValue > newValue ? currentValue - newValue : newValue - currentValue;
+ formatString = new string('0', RequiredDisplayDigits.Value);
+ UpdateDisplay();
}
- protected override LocalisableString FormatCount(double count)
- {
- string format = new string('0', RequiredDisplayDigits.Value);
+ protected override double GetProportionalDuration(double currentValue, double newValue) =>
+ currentValue > newValue ? currentValue - newValue : newValue - currentValue;
- if (UseCommaSeparator)
- {
- for (int i = format.Length - 3; i > 0; i -= 3)
- format = format.Insert(i, @",");
- }
-
- return ((long)count).ToString(format);
- }
+ protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(formatString);
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true));
diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
new file mode 100644
index 0000000000..27e28f1e03
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Graphics.UserInterfaceV2
+{
+ public class RoundedButton : OsuButton, IFilterable
+ {
+ public override float Height
+ {
+ get => base.Height;
+ set
+ {
+ base.Height = value;
+
+ if (IsLoaded)
+ updateCornerRadius();
+ }
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ BackgroundColour = colours.Blue3;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateCornerRadius();
+ }
+
+ private void updateCornerRadius() => Content.CornerRadius = DrawHeight / 2;
+
+ public virtual IEnumerable FilterTerms => new[] { Text.ToString() };
+
+ public bool MatchingFilter
+ {
+ set => this.FadeTo(value ? 1 : 0);
+ }
+
+ public bool FilteringActive { get; set; }
+ }
+}
diff --git a/osu.Game/Localisation/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs
index aa6eabd7d1..008781c2e5 100644
--- a/osu.Game/Localisation/AudioSettingsStrings.cs
+++ b/osu.Game/Localisation/AudioSettingsStrings.cs
@@ -24,6 +24,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString VolumeHeader => new TranslatableString(getKey(@"volume_header"), @"Volume");
+ ///
+ /// "Output device"
+ ///
+ public static LocalisableString OutputDevice => new TranslatableString(getKey(@"output_device"), @"Output device");
+
///
/// "Master"
///
diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs
index 6d6381b429..fa92187650 100644
--- a/osu.Game/Localisation/GameplaySettingsStrings.cs
+++ b/osu.Game/Localisation/GameplaySettingsStrings.cs
@@ -14,11 +14,36 @@ namespace osu.Game.Localisation
///
public static LocalisableString GameplaySectionHeader => new TranslatableString(getKey(@"gameplay_section_header"), @"Gameplay");
+ ///
+ /// "Beatmap"
+ ///
+ public static LocalisableString BeatmapHeader => new TranslatableString(getKey(@"beatmap_header"), @"Beatmap");
+
///
/// "General"
///
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
+ ///
+ /// "Audio"
+ ///
+ public static LocalisableString AudioHeader => new TranslatableString(getKey(@"audio"), @"Audio");
+
+ ///
+ /// "HUD"
+ ///
+ public static LocalisableString HUDHeader => new TranslatableString(getKey(@"h_u_d"), @"HUD");
+
+ ///
+ /// "Input"
+ ///
+ public static LocalisableString InputHeader => new TranslatableString(getKey(@"input"), @"Input");
+
+ ///
+ /// "Background"
+ ///
+ public static LocalisableString BackgroundHeader => new TranslatableString(getKey(@"background"), @"Background");
+
///
/// "Background dim"
///
diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs
index 0e384f983f..f85cc0f2ae 100644
--- a/osu.Game/Localisation/GraphicsSettingsStrings.cs
+++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs
@@ -104,6 +104,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString HitLighting => new TranslatableString(getKey(@"hit_lighting"), @"Hit lighting");
+ ///
+ /// "Screenshots"
+ ///
+ public static LocalisableString Screenshots => new TranslatableString(getKey(@"screenshots"), @"Screenshots");
+
///
/// "Screenshot format"
///
diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs
new file mode 100644
index 0000000000..a356c9e20b
--- /dev/null
+++ b/osu.Game/Localisation/RulesetSettingsStrings.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . 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 RulesetSettingsStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.RulesetSettings";
+
+ ///
+ /// "Rulesets"
+ ///
+ public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs
index f22b4d6bf5..8b74b94d59 100644
--- a/osu.Game/Localisation/SkinSettingsStrings.cs
+++ b/osu.Game/Localisation/SkinSettingsStrings.cs
@@ -14,6 +14,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString SkinSectionHeader => new TranslatableString(getKey(@"skin_section_header"), @"Skin");
+ ///
+ /// "Current skin"
+ ///
+ public static LocalisableString CurrentSkin => new TranslatableString(getKey(@"current_skin"), @"Current skin");
+
///
/// "Skin layout editor"
///
diff --git a/osu.Game/Models/RealmBeatmap.cs b/osu.Game/Models/RealmBeatmap.cs
new file mode 100644
index 0000000000..5049c1384d
--- /dev/null
+++ b/osu.Game/Models/RealmBeatmap.cs
@@ -0,0 +1,117 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Rulesets;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ ///
+ /// A single beatmap difficulty.
+ ///
+ [ExcludeFromDynamicCompile]
+ [Serializable]
+ [MapTo("Beatmap")]
+ public class RealmBeatmap : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo
+ {
+ [PrimaryKey]
+ public Guid ID { get; set; } = Guid.NewGuid();
+
+ public string DifficultyName { get; set; } = string.Empty;
+
+ public RealmRuleset Ruleset { get; set; } = null!;
+
+ public RealmBeatmapDifficulty Difficulty { get; set; } = null!;
+
+ public RealmBeatmapMetadata Metadata { get; set; } = null!;
+
+ public RealmBeatmapSet? BeatmapSet { get; set; }
+
+ public BeatmapSetOnlineStatus Status
+ {
+ get => (BeatmapSetOnlineStatus)StatusInt;
+ set => StatusInt = (int)value;
+ }
+
+ [MapTo(nameof(Status))]
+ public int StatusInt { get; set; }
+
+ public int? OnlineID { get; set; }
+
+ public double Length { get; set; }
+
+ public double BPM { get; set; }
+
+ public string Hash { get; set; } = string.Empty;
+
+ public double StarRating { get; set; }
+
+ public string MD5Hash { get; set; } = string.Empty;
+
+ [JsonIgnore]
+ public bool Hidden { get; set; }
+
+ public RealmBeatmap(RealmRuleset ruleset, RealmBeatmapDifficulty difficulty, RealmBeatmapMetadata metadata)
+ {
+ Ruleset = ruleset;
+ Difficulty = difficulty;
+ Metadata = metadata;
+ }
+
+ [UsedImplicitly]
+ private RealmBeatmap()
+ {
+ }
+
+ #region Properties we may not want persisted (but also maybe no harm?)
+
+ public double AudioLeadIn { get; set; }
+
+ public float StackLeniency { get; set; } = 0.7f;
+
+ public bool SpecialStyle { get; set; }
+
+ public bool LetterboxInBreaks { get; set; }
+
+ public bool WidescreenStoryboard { get; set; }
+
+ public bool EpilepsyWarning { get; set; }
+
+ public bool SamplesMatchPlaybackRate { get; set; }
+
+ public double DistanceSpacing { get; set; }
+
+ public int BeatDivisor { get; set; }
+
+ public int GridSize { get; set; }
+
+ public double TimelineZoom { get; set; }
+
+ #endregion
+
+ public bool AudioEquals(RealmBeatmap? other) => other != null
+ && BeatmapSet != null
+ && other.BeatmapSet != null
+ && BeatmapSet.Hash == other.BeatmapSet.Hash
+ && Metadata.AudioFile == other.Metadata.AudioFile;
+
+ public bool BackgroundEquals(RealmBeatmap? other) => other != null
+ && BeatmapSet != null
+ && other.BeatmapSet != null
+ && BeatmapSet.Hash == other.BeatmapSet.Hash
+ && Metadata.BackgroundFile == other.Metadata.BackgroundFile;
+
+ IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
+ IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
+ IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
+ IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => Difficulty;
+ }
+}
diff --git a/osu.Game/Models/RealmBeatmapDifficulty.cs b/osu.Game/Models/RealmBeatmapDifficulty.cs
new file mode 100644
index 0000000000..3c1dad69e4
--- /dev/null
+++ b/osu.Game/Models/RealmBeatmapDifficulty.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [MapTo("BeatmapDifficulty")]
+ public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo
+ {
+ public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
+ public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
+ public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
+ public float ApproachRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
+
+ public double SliderMultiplier { get; set; } = 1;
+ public double SliderTickRate { get; set; } = 1;
+
+ ///
+ /// Returns a shallow-clone of this .
+ ///
+ public RealmBeatmapDifficulty Clone()
+ {
+ var diff = new RealmBeatmapDifficulty();
+ CopyTo(diff);
+ return diff;
+ }
+
+ public void CopyTo(RealmBeatmapDifficulty difficulty)
+ {
+ difficulty.ApproachRate = ApproachRate;
+ difficulty.DrainRate = DrainRate;
+ difficulty.CircleSize = CircleSize;
+ difficulty.OverallDifficulty = OverallDifficulty;
+
+ difficulty.SliderMultiplier = SliderMultiplier;
+ difficulty.SliderTickRate = SliderTickRate;
+ }
+ }
+}
diff --git a/osu.Game/Models/RealmBeatmapMetadata.cs b/osu.Game/Models/RealmBeatmapMetadata.cs
new file mode 100644
index 0000000000..6ea7170d0f
--- /dev/null
+++ b/osu.Game/Models/RealmBeatmapMetadata.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using Newtonsoft.Json;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [Serializable]
+ [MapTo("BeatmapMetadata")]
+ public class RealmBeatmapMetadata : RealmObject, IBeatmapMetadataInfo
+ {
+ public string Title { get; set; } = string.Empty;
+
+ [JsonProperty("title_unicode")]
+ public string TitleUnicode { get; set; } = string.Empty;
+
+ public string Artist { get; set; } = string.Empty;
+
+ [JsonProperty("artist_unicode")]
+ public string ArtistUnicode { get; set; } = string.Empty;
+
+ public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User.
+
+ public string Source { get; set; } = string.Empty;
+
+ [JsonProperty(@"tags")]
+ public string Tags { get; set; } = string.Empty;
+
+ ///
+ /// The time in milliseconds to begin playing the track for preview purposes.
+ /// If -1, the track should begin playing at 40% of its length.
+ ///
+ public int PreviewTime { get; set; }
+
+ public string AudioFile { get; set; } = string.Empty;
+ public string BackgroundFile { get; set; } = string.Empty;
+ }
+}
diff --git a/osu.Game/Models/RealmBeatmapSet.cs b/osu.Game/Models/RealmBeatmapSet.cs
new file mode 100644
index 0000000000..314ca4494b
--- /dev/null
+++ b/osu.Game/Models/RealmBeatmapSet.cs
@@ -0,0 +1,78 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [MapTo("BeatmapSet")]
+ public class RealmBeatmapSet : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IBeatmapSetInfo
+ {
+ [PrimaryKey]
+ public Guid ID { get; set; } = Guid.NewGuid();
+
+ public int? OnlineID { get; set; }
+
+ public DateTimeOffset DateAdded { get; set; }
+
+ public IBeatmapMetadataInfo? Metadata => Beatmaps.FirstOrDefault()?.Metadata;
+
+ public IList Beatmaps { get; } = null!;
+
+ public IList Files { get; } = null!;
+
+ public bool DeletePending { get; set; }
+
+ public string Hash { get; set; } = string.Empty;
+
+ ///
+ /// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present).
+ ///
+ public bool Protected { get; set; }
+
+ public double MaxStarDifficulty => Beatmaps.Max(b => b.StarRating);
+
+ public double MaxLength => Beatmaps.Max(b => b.Length);
+
+ public double MaxBPM => Beatmaps.Max(b => b.BPM);
+
+ ///
+ /// 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 name of the file to get the storage path of.
+ public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.StoragePath;
+
+ public override string ToString() => Metadata?.ToString() ?? base.ToString();
+
+ public bool Equals(RealmBeatmapSet? other)
+ {
+ if (other == null)
+ return false;
+
+ if (IsManaged && other.IsManaged)
+ return ID == other.ID;
+
+ if (OnlineID.HasValue && other.OnlineID.HasValue)
+ return OnlineID == other.OnlineID;
+
+ if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
+ return Hash == other.Hash;
+
+ return ReferenceEquals(this, other);
+ }
+
+ IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps;
+
+ IEnumerable IBeatmapSetInfo.Files => Files;
+ }
+}
diff --git a/osu.Game/Models/RealmFile.cs b/osu.Game/Models/RealmFile.cs
new file mode 100644
index 0000000000..2715f4be45
--- /dev/null
+++ b/osu.Game/Models/RealmFile.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using osu.Framework.Testing;
+using osu.Game.IO;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [MapTo("File")]
+ public class RealmFile : RealmObject, IFileInfo
+ {
+ [PrimaryKey]
+ public string Hash { get; set; } = string.Empty;
+
+ public string StoragePath => Path.Combine(Hash.Remove(1), Hash.Remove(2), Hash);
+ }
+}
diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs
new file mode 100644
index 0000000000..ba12d51d0b
--- /dev/null
+++ b/osu.Game/Models/RealmNamedFileUsage.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Framework.Testing;
+using osu.Game.Database;
+using osu.Game.IO;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ public class RealmNamedFileUsage : EmbeddedObject, INamedFile, INamedFileUsage
+ {
+ public RealmFile File { get; set; } = null!;
+
+ public string Filename { get; set; } = null!;
+
+ public RealmNamedFileUsage(RealmFile file, string filename)
+ {
+ File = file;
+ Filename = filename;
+ }
+
+ [UsedImplicitly]
+ private RealmNamedFileUsage()
+ {
+ }
+
+ IFileInfo INamedFileUsage.File => File;
+ }
+}
diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Models/RealmRuleset.cs
new file mode 100644
index 0000000000..0dcd701ed2
--- /dev/null
+++ b/osu.Game/Models/RealmRuleset.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using JetBrains.Annotations;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [MapTo("Ruleset")]
+ public class RealmRuleset : RealmObject, IEquatable, IRulesetInfo
+ {
+ [PrimaryKey]
+ public string ShortName { get; set; } = string.Empty;
+
+ public int? OnlineID { get; set; }
+
+ public string Name { get; set; } = string.Empty;
+
+ public string InstantiationInfo { get; set; } = string.Empty;
+
+ public RealmRuleset(string shortName, string name, string instantiationInfo, int? onlineID = null)
+ {
+ ShortName = shortName;
+ Name = name;
+ InstantiationInfo = instantiationInfo;
+ OnlineID = onlineID;
+ }
+
+ [UsedImplicitly]
+ private RealmRuleset()
+ {
+ }
+
+ public RealmRuleset(int? onlineID, string name, string shortName, bool available)
+ {
+ OnlineID = onlineID;
+ Name = name;
+ ShortName = shortName;
+ Available = available;
+ }
+
+ public bool Available { get; set; }
+
+ public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo;
+
+ public override string ToString() => Name;
+
+ public RealmRuleset Clone() => new RealmRuleset
+ {
+ OnlineID = OnlineID,
+ Name = Name,
+ ShortName = ShortName,
+ InstantiationInfo = InstantiationInfo,
+ Available = Available
+ };
+ }
+}
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index 0e4ea694aa..201ba6239b 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -177,6 +177,24 @@ namespace osu.Game.Online.Chat
case "wiki":
return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3)));
+
+ case "home":
+ if (mainArg != "changelog")
+ // handle link other than changelog as external for now
+ return new LinkDetails(LinkAction.External, url);
+
+ switch (args.Length)
+ {
+ case 4:
+ // https://osu.ppy.sh/home/changelog
+ return new LinkDetails(LinkAction.OpenChangelog, string.Empty);
+
+ case 6:
+ // https://osu.ppy.sh/home/changelog/lazer/2021.1006
+ return new LinkDetails(LinkAction.OpenChangelog, $"{args[4]}/{args[5]}");
+ }
+
+ break;
}
}
@@ -324,6 +342,7 @@ namespace osu.Game.Online.Chat
SearchBeatmapSet,
OpenWiki,
Custom,
+ OpenChangelog,
}
public class Link : IComparable
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 8a018f17d9..020cdebab6 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -90,6 +90,8 @@ namespace osu.Game
private WikiOverlay wikiOverlay;
+ private ChangelogOverlay changelogOverlay;
+
private SkinEditorOverlay skinEditor;
private Container overlayContent;
@@ -209,13 +211,6 @@ namespace osu.Game
[BackgroundDependencyLoader]
private void load()
{
- if (args?.Length > 0)
- {
- var paths = args.Where(a => !a.StartsWith('-')).ToArray();
- if (paths.Length > 0)
- Task.Run(() => Import(paths));
- }
-
dependencies.CacheAs(this);
dependencies.Cache(SentryLogger);
@@ -336,6 +331,17 @@ namespace osu.Game
ShowWiki(link.Argument);
break;
+ case LinkAction.OpenChangelog:
+ if (string.IsNullOrEmpty(link.Argument))
+ ShowChangelogListing();
+ else
+ {
+ var changelogArgs = link.Argument.Split("/");
+ ShowChangelogBuild(changelogArgs[0], changelogArgs[1]);
+ }
+
+ break;
+
default:
throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
}
@@ -401,6 +407,18 @@ namespace osu.Game
/// The wiki page to show
public void ShowWiki(string path) => waitForReady(() => wikiOverlay, _ => wikiOverlay.ShowPage(path));
+ ///
+ /// Show changelog listing overlay
+ ///
+ public void ShowChangelogListing() => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowListing());
+
+ ///
+ /// Show changelog's build as an overlay
+ ///
+ /// The update stream name
+ /// The build version of the update stream
+ public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
+
///
/// Present a beatmap at song select immediately.
/// The user should have already requested this interactively.
@@ -769,7 +787,7 @@ namespace osu.Game
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);
- var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true);
+ loadComponentSingleFile(changelogOverlay = new ChangelogOverlay(), overlayContent.Add, true);
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true);
@@ -842,6 +860,19 @@ namespace osu.Game
{
if (mode.NewValue != OverlayActivation.All) CloseAllOverlays();
};
+
+ // Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup.
+ handleStartupImport();
+ }
+
+ private void handleStartupImport()
+ {
+ if (args?.Length > 0)
+ {
+ var paths = args.Where(a => !a.StartsWith('-')).ToArray();
+ if (paths.Length > 0)
+ Task.Run(() => Import(paths));
+ }
}
private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 7f4fe8a943..09eb482d16 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@@ -410,11 +411,28 @@ namespace osu.Game
{
Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""...");
- using (realmFactory.BlockAllOperations())
+ IDisposable realmBlocker = null;
+
+ try
{
- contextFactory.FlushConnections();
+ ManualResetEventSlim readyToRun = new ManualResetEventSlim();
+
+ Scheduler.Add(() =>
+ {
+ realmBlocker = realmFactory.BlockAllOperations();
+ contextFactory.FlushConnections();
+
+ readyToRun.Set();
+ }, false);
+
+ readyToRun.Wait();
+
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
}
+ finally
+ {
+ realmBlocker?.Dispose();
+ }
Logger.Log(@"Migration complete!");
}
diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs
index 51214fe460..198aa1438a 100644
--- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs
+++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Overlays.OSD
private Sample sampleChange;
public TrackedSettingToast(SettingDescription description)
- : base(description.Name, description.Value, description.Shortcut)
+ : base(description.Name.ToString(), description.Value.ToString(), description.Shortcut.ToString())
{
FillFlowContainer optionLights;
diff --git a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs
index c02db40eca..4ca3ace8a1 100644
--- a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs
+++ b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs
@@ -14,10 +14,7 @@ namespace osu.Game.Overlays.Settings
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- BackgroundColour = colours.Pink;
-
- Triangles.ColourDark = colours.PinkDark;
- Triangles.ColourLight = colours.PinkLight;
+ BackgroundColour = colours.Pink3;
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
index d697b45424..0c54ae2763 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
@@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
dropdown = new AudioDeviceSettingsDropdown
{
+ LabelText = AudioSettingsStrings.OutputDevice,
Keywords = new[] { "speaker", "headphone", "output" }
}
};
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs
new file mode 100644
index 0000000000..dba64d695a
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections.Gameplay
+{
+ public class AudioSettings : SettingsSubsection
+ {
+ protected override LocalisableString Header => GameplaySettingsStrings.AudioHeader;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ Children = new Drawable[]
+ {
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.PositionalHitsounds,
+ Current = config.GetBindable(OsuSetting.PositionalHitSounds)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak,
+ Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak)
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs
new file mode 100644
index 0000000000..94e0c5e494
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections.Gameplay
+{
+ public class BackgroundSettings : SettingsSubsection
+ {
+ protected override LocalisableString Header => GameplaySettingsStrings.BackgroundHeader;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ Children = new Drawable[]
+ {
+ new SettingsSlider
+ {
+ LabelText = GameplaySettingsStrings.BackgroundDim,
+ Current = config.GetBindable(OsuSetting.DimLevel),
+ KeyboardStep = 0.01f,
+ DisplayAsPercentage = true
+ },
+ new SettingsSlider
+ {
+ LabelText = GameplaySettingsStrings.BackgroundBlur,
+ Current = config.GetBindable(OsuSetting.BlurLevel),
+ KeyboardStep = 0.01f,
+ DisplayAsPercentage = true
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.LightenDuringBreaks,
+ Current = config.GetBindable(OsuSetting.LightenDuringBreaks)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.FadePlayfieldWhenHealthLow,
+ Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow),
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs
new file mode 100644
index 0000000000..aaa60ce81b
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections.Gameplay
+{
+ public class BeatmapSettings : SettingsSubsection
+ {
+ protected override LocalisableString Header => GameplaySettingsStrings.BeatmapHeader;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ Children = new Drawable[]
+ {
+ new SettingsCheckbox
+ {
+ LabelText = SkinSettingsStrings.BeatmapSkins,
+ Current = config.GetBindable(OsuSetting.BeatmapSkins)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = SkinSettingsStrings.BeatmapColours,
+ Current = config.GetBindable(OsuSetting.BeatmapColours)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = SkinSettingsStrings.BeatmapHitsounds,
+ Current = config.GetBindable(OsuSetting.BeatmapHitsounds)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GraphicsSettingsStrings.StoryboardVideo,
+ Current = config.GetBindable(OsuSetting.ShowStoryboard)
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
index 3a0265e453..d4e4fd571d 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
@@ -1,7 +1,6 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
@@ -20,77 +19,18 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
{
Children = new Drawable[]
{
- new SettingsSlider
- {
- LabelText = GameplaySettingsStrings.BackgroundDim,
- Current = config.GetBindable(OsuSetting.DimLevel),
- KeyboardStep = 0.01f,
- DisplayAsPercentage = true
- },
- new SettingsSlider
- {
- LabelText = GameplaySettingsStrings.BackgroundBlur,
- Current = config.GetBindable(OsuSetting.BlurLevel),
- KeyboardStep = 0.01f,
- DisplayAsPercentage = true
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.LightenDuringBreaks,
- Current = config.GetBindable(OsuSetting.LightenDuringBreaks)
- },
- new SettingsEnumDropdown
- {
- LabelText = GameplaySettingsStrings.HUDVisibilityMode,
- Current = config.GetBindable(OsuSetting.HUDVisibilityMode)
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.ShowDifficultyGraph,
- Current = config.GetBindable(OsuSetting.ShowProgressGraph)
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail,
- Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail),
- Keywords = new[] { "hp", "bar" }
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.FadePlayfieldWhenHealthLow,
- Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow),
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay,
- Current = config.GetBindable(OsuSetting.KeyOverlay)
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.PositionalHitsounds,
- Current = config.GetBindable(OsuSetting.PositionalHitSounds)
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak,
- Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak)
- },
new SettingsEnumDropdown
{
LabelText = GameplaySettingsStrings.ScoreDisplayMode,
Current = config.GetBindable(OsuSetting.ScoreDisplayMode),
Keywords = new[] { "scoring" }
},
- };
-
- if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
- {
- Add(new SettingsCheckbox
+ new SettingsCheckbox
{
- LabelText = GameplaySettingsStrings.DisableWinKey,
- Current = config.GetBindable(OsuSetting.GameplayDisableWinKey)
- });
- }
+ LabelText = GraphicsSettingsStrings.HitLighting,
+ Current = config.GetBindable(OsuSetting.HitLighting)
+ },
+ };
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs
new file mode 100644
index 0000000000..e1b452e322
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections.Gameplay
+{
+ public class HUDSettings : SettingsSubsection
+ {
+ protected override LocalisableString Header => GameplaySettingsStrings.HUDHeader;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ Children = new Drawable[]
+ {
+ new SettingsEnumDropdown
+ {
+ LabelText = GameplaySettingsStrings.HUDVisibilityMode,
+ Current = config.GetBindable(OsuSetting.HUDVisibilityMode)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.ShowDifficultyGraph,
+ Current = config.GetBindable(OsuSetting.ShowProgressGraph)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail,
+ Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail),
+ Keywords = new[] { "hp", "bar" }
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay,
+ Current = config.GetBindable(OsuSetting.KeyOverlay)
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs
new file mode 100644
index 0000000000..962572ca6e
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections.Gameplay
+{
+ public class InputSettings : SettingsSubsection
+ {
+ protected override LocalisableString Header => GameplaySettingsStrings.InputHeader;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ Children = new Drawable[]
+ {
+ new SettingsSlider
+ {
+ LabelText = SkinSettingsStrings.GameplayCursorSize,
+ Current = config.GetBindable(OsuSetting.GameplayCursorSize),
+ KeyboardStep = 0.01f
+ },
+ new SettingsCheckbox
+ {
+ LabelText = SkinSettingsStrings.AutoCursorSize,
+ Current = config.GetBindable(OsuSetting.AutoCursorSize)
+ },
+ };
+
+ if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
+ {
+ Add(new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.DisableWinKey,
+ Current = config.GetBindable(OsuSetting.GameplayDisableWinKey)
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
index 42d9d48d73..120e2d908c 100644
--- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
@@ -1,16 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Game.Overlays.Settings.Sections.Gameplay;
-using osu.Game.Rulesets;
-using System.Linq;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Logging;
using osu.Framework.Localisation;
using osu.Game.Localisation;
+using osu.Game.Overlays.Settings.Sections.Gameplay;
namespace osu.Game.Overlays.Settings.Sections
{
@@ -20,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections
public override Drawable CreateIcon() => new SpriteIcon
{
- Icon = FontAwesome.Regular.Circle
+ Icon = FontAwesome.Regular.DotCircle
};
public GameplaySection()
@@ -28,27 +23,13 @@ namespace osu.Game.Overlays.Settings.Sections
Children = new Drawable[]
{
new GeneralSettings(),
+ new AudioSettings(),
+ new BeatmapSettings(),
+ new BackgroundSettings(),
+ new HUDSettings(),
+ new InputSettings(),
new ModsSettings(),
};
}
-
- [BackgroundDependencyLoader]
- private void load(RulesetStore rulesets)
- {
- foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance()))
- {
- try
- {
- SettingsSubsection section = ruleset.CreateSettings();
-
- if (section != null)
- Add(section);
- }
- catch (Exception e)
- {
- Logger.Error(e, "Failed to load ruleset settings");
- }
- }
- }
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs
similarity index 67%
rename from osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs
rename to osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs
index 20b1d8d801..dbb9ddc1c1 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs
@@ -9,25 +9,15 @@ using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Graphics
{
- public class DetailSettings : SettingsSubsection
+ public class ScreenshotSettings : SettingsSubsection
{
- protected override LocalisableString Header => GraphicsSettingsStrings.DetailSettingsHeader;
+ protected override LocalisableString Header => GraphicsSettingsStrings.Screenshots;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
Children = new Drawable[]
{
- new SettingsCheckbox
- {
- LabelText = GraphicsSettingsStrings.StoryboardVideo,
- Current = config.GetBindable(OsuSetting.ShowStoryboard)
- },
- new SettingsCheckbox
- {
- LabelText = GraphicsSettingsStrings.HitLighting,
- Current = config.GetBindable(OsuSetting.HitLighting)
- },
new SettingsEnumDropdown
{
LabelText = GraphicsSettingsStrings.ScreenshotFormat,
diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
index fd0718f9f2..591848506a 100644
--- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
@@ -22,9 +22,9 @@ namespace osu.Game.Overlays.Settings.Sections
{
Children = new Drawable[]
{
- new RendererSettings(),
new LayoutSettings(),
- new DetailSettings(),
+ new RendererSettings(),
+ new ScreenshotSettings(),
};
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
index cf8adf2785..da789db79a 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
@@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private RealmContextFactory realmFactory { get; set; }
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -101,7 +101,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
EdgeEffect = new EdgeEffectParameters
{
Radius = 2,
- Colour = colours.YellowDark.Opacity(0),
+ Colour = colourProvider.Highlight1.Opacity(0),
Type = EdgeEffectType.Shadow,
Hollow = true,
},
@@ -110,13 +110,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black,
- Alpha = 0.6f,
+ Colour = colourProvider.Background5,
},
text = new OsuSpriteText
{
Text = action.GetLocalisableDescription(),
- Margin = new MarginPadding(padding),
+ Margin = new MarginPadding(1.5f * padding),
},
buttons = new FillFlowContainer
{
@@ -405,7 +404,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private readonly Box box;
public readonly OsuSpriteText Text;
- private Color4 hoverColour;
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
private bool isBinding;
@@ -448,7 +448,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
box = new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black
},
Text = new OsuSpriteText
{
@@ -463,9 +462,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load()
{
- hoverColour = colours.YellowDark;
+ updateHoverState();
}
protected override bool OnHover(HoverEvent e)
@@ -484,12 +483,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
if (isBinding)
{
- box.FadeColour(Color4.White, transition_time, Easing.OutQuint);
+ box.FadeColour(colourProvider.Light2, transition_time, Easing.OutQuint);
Text.FadeColour(Color4.Black, transition_time, Easing.OutQuint);
}
else
{
- box.FadeColour(IsHovered ? hoverColour : Color4.Black, transition_time, Easing.OutQuint);
+ box.FadeColour(IsHovered ? colourProvider.Light4 : colourProvider.Background6, transition_time, Easing.OutQuint);
Text.FadeColour(IsHovered ? Color4.Black : Color4.White, transition_time, Easing.OutQuint);
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
index 2cc2857e9b..2051af6f3c 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
@@ -7,7 +7,6 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Game.Database;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Localisation;
@@ -27,8 +26,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
this.variant = variant;
- FlowContent.Spacing = new Vector2(0, 1);
- FlowContent.Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS };
+ FlowContent.Spacing = new Vector2(0, 3);
}
[BackgroundDependencyLoader]
@@ -60,7 +58,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
}
- public class ResetButton : DangerousTriangleButton
+ public class ResetButton : DangerousSettingsButton
{
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs
index 26610628d5..3ef5ce8941 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -8,16 +9,24 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Handlers.Tablet;
using osu.Game.Graphics;
-using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Overlays.Settings.Sections.Input
{
- internal class RotationPresetButtons : FillFlowContainer
+ internal class RotationPresetButtons : CompositeDrawable
{
+ public new MarginPadding Padding
+ {
+ get => base.Padding;
+ set => base.Padding = value;
+ }
+
private readonly ITabletHandler tabletHandler;
private Bindable rotation;
+ private readonly RotationButton[] rotationPresets = new RotationButton[preset_count];
+ private const int preset_count = 4;
private const int height = 50;
public RotationPresetButtons(ITabletHandler tabletHandler)
@@ -27,18 +36,39 @@ namespace osu.Game.Overlays.Settings.Sections.Input
RelativeSizeAxes = Axes.X;
Height = height;
- for (int i = 0; i < 360; i += 90)
+ IEnumerable createColumns(int count)
{
- var presetRotation = i;
-
- Add(new RotationButton(i)
+ for (int i = 0; i < count; ++i)
{
- RelativeSizeAxes = Axes.X,
- Height = height,
- Width = 0.25f,
- Text = $@"{presetRotation}º",
- Action = () => tabletHandler.Rotation.Value = presetRotation,
- });
+ if (i > 0)
+ yield return new Dimension(GridSizeMode.Absolute, 10);
+
+ yield return new Dimension();
+ }
+ }
+
+ GridContainer grid;
+
+ InternalChild = grid = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = createColumns(preset_count).ToArray()
+ };
+
+ grid.Content = new[] { new Drawable[preset_count * 2 - 1] };
+
+ for (int i = 0; i < preset_count; i++)
+ {
+ var rotationValue = i * 90;
+
+ var rotationPreset = new RotationButton(rotationValue)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 1,
+ Text = $@"{rotationValue}º",
+ Action = () => tabletHandler.Rotation.Value = rotationValue,
+ };
+ grid.Content[0][2 * i] = rotationPresets[i] = rotationPreset;
}
}
@@ -49,16 +79,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input
rotation = tabletHandler.Rotation.GetBoundCopy();
rotation.BindValueChanged(val =>
{
- foreach (var b in Children.OfType())
+ foreach (var b in rotationPresets)
b.IsSelected = b.Preset == val.NewValue;
}, true);
}
- public class RotationButton : TriangleButton
+ public class RotationButton : RoundedButton
{
[Resolved]
private OsuColour colours { get; set; }
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
+
public readonly int Preset;
public RotationButton(int preset)
@@ -91,18 +124,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private void updateColour()
{
- if (isSelected)
- {
- BackgroundColour = colours.BlueDark;
- Triangles.ColourDark = colours.BlueDarker;
- Triangles.ColourLight = colours.Blue;
- }
- else
- {
- BackgroundColour = colours.Gray4;
- Triangles.ColourDark = colours.Gray5;
- Triangles.ColourLight = colours.Gray6;
- }
+ BackgroundColour = isSelected ? colours.Blue3 : colourProvider.Background3;
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 8c60e81fb5..c94b418331 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -165,7 +165,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
LabelText = TabletSettingsStrings.Rotation,
Current = rotation
},
- new RotationPresetButtons(tabletHandler),
+ new RotationPresetButtons(tabletHandler)
+ {
+ Padding = new MarginPadding
+ {
+ Horizontal = SettingsPanel.CONTENT_MARGINS
+ }
+ },
new SettingsSlider
{
TransferValueOnCommit = true,
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
index 803c8332c1..43df58a8b1 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
@@ -10,7 +10,6 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Scoring;
using osu.Game.Skinning;
@@ -21,15 +20,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
protected override LocalisableString Header => "General";
- private TriangleButton importBeatmapsButton;
- private TriangleButton importScoresButton;
- private TriangleButton importSkinsButton;
- private TriangleButton importCollectionsButton;
- private TriangleButton deleteBeatmapsButton;
- private TriangleButton deleteScoresButton;
- private TriangleButton deleteSkinsButton;
- private TriangleButton restoreButton;
- private TriangleButton undeleteButton;
+ private SettingsButton importBeatmapsButton;
+ private SettingsButton importScoresButton;
+ private SettingsButton importSkinsButton;
+ private SettingsButton importCollectionsButton;
+ private SettingsButton deleteBeatmapsButton;
+ private SettingsButton deleteScoresButton;
+ private SettingsButton deleteSkinsButton;
+ private SettingsButton restoreButton;
+ private SettingsButton undeleteButton;
[BackgroundDependencyLoader(permitNulls: true)]
private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] StableImportManager stableImportManager, DialogOverlay dialogOverlay)
diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
index fa0c06167b..9410a87848 100644
--- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
@@ -6,7 +6,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings.Sections.Maintenance;
-using osuTK;
namespace osu.Game.Overlays.Settings.Sections
{
@@ -21,7 +20,6 @@ namespace osu.Game.Overlays.Settings.Sections
public MaintenanceSection()
{
- FlowContent.Spacing = new Vector2(0, 5);
Children = new Drawable[]
{
new GeneralSettings()
diff --git a/osu.Game/Overlays/Settings/Sections/RulesetSection.cs b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs
new file mode 100644
index 0000000000..b9339d5299
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Framework.Logging;
+using osu.Game.Rulesets;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections
+{
+ public class RulesetSection : SettingsSection
+ {
+ public override LocalisableString Header => RulesetSettingsStrings.Rulesets;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Chess
+ };
+
+ [BackgroundDependencyLoader]
+ private void load(RulesetStore rulesets)
+ {
+ foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance()))
+ {
+ try
+ {
+ SettingsSubsection section = ruleset.CreateSettings();
+
+ if (section != null)
+ Add(section);
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Failed to load ruleset settings");
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index e0d8252930..00198235c5 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -16,7 +16,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
-using osuTK;
namespace osu.Game.Overlays.Settings.Sections
{
@@ -63,43 +62,18 @@ namespace osu.Game.Overlays.Settings.Sections
[BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor)
{
- FlowContent.Spacing = new Vector2(0, 5);
-
Children = new Drawable[]
{
- skinDropdown = new SkinSettingsDropdown(),
+ skinDropdown = new SkinSettingsDropdown
+ {
+ LabelText = SkinSettingsStrings.CurrentSkin
+ },
new SettingsButton
{
Text = SkinSettingsStrings.SkinLayoutEditor,
Action = () => skinEditor?.Toggle(),
},
new ExportSkinButton(),
- new SettingsSlider
- {
- LabelText = SkinSettingsStrings.GameplayCursorSize,
- Current = config.GetBindable(OsuSetting.GameplayCursorSize),
- KeyboardStep = 0.01f
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.AutoCursorSize,
- Current = config.GetBindable(OsuSetting.AutoCursorSize)
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.BeatmapSkins,
- Current = config.GetBindable(OsuSetting.BeatmapSkins)
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.BeatmapColours,
- Current = config.GetBindable(OsuSetting.BeatmapColours)
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.BeatmapHitsounds,
- Current = config.GetBindable(OsuSetting.BeatmapHitsounds)
- },
};
managerUpdated = skins.ItemUpdated.GetBoundCopy();
diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs
index 87b1aa0e46..be7f2de480 100644
--- a/osu.Game/Overlays/Settings/SettingsButton.cs
+++ b/osu.Game/Overlays/Settings/SettingsButton.cs
@@ -6,11 +6,11 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
-using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Overlays.Settings
{
- public class SettingsButton : TriangleButton, IHasTooltip
+ public class SettingsButton : RoundedButton, IHasTooltip
{
public SettingsButton()
{
diff --git a/osu.Game/Overlays/Settings/SettingsHeader.cs b/osu.Game/Overlays/Settings/SettingsHeader.cs
index a7f1cef74c..69b7b69a29 100644
--- a/osu.Game/Overlays/Settings/SettingsHeader.cs
+++ b/osu.Game/Overlays/Settings/SettingsHeader.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Settings
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Settings
new OsuSpriteText
{
Text = heading,
- Font = OsuFont.GetFont(size: 40),
+ Font = OsuFont.TorusAlternate.With(size: 40),
Margin = new MarginPadding
{
Left = SettingsPanel.CONTENT_MARGINS,
@@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Settings
},
new OsuSpriteText
{
- Colour = colours.Pink,
+ Colour = colourProvider.Content2,
Text = subheading,
Font = OsuFont.GetFont(size: 18),
Margin = new MarginPadding
diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs
index 2a6f3f5ed7..0ae353602e 100644
--- a/osu.Game/Overlays/Settings/SettingsSection.cs
+++ b/osu.Game/Overlays/Settings/SettingsSection.cs
@@ -12,7 +12,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
-using osuTK.Graphics;
+using osuTK;
namespace osu.Game.Overlays.Settings
{
@@ -31,9 +31,10 @@ namespace osu.Game.Overlays.Settings
public IEnumerable FilterableChildren => Children.OfType();
public virtual IEnumerable FilterTerms => new[] { Header.ToString() };
- private const int header_size = 26;
- private const int margin = 20;
- private const int border_size = 2;
+ public const int ITEM_SPACING = 14;
+
+ private const int header_size = 24;
+ private const int border_size = 4;
public bool MatchingFilter
{
@@ -54,8 +55,9 @@ namespace osu.Game.Overlays.Settings
{
Margin = new MarginPadding
{
- Top = header_size
+ Top = 36
},
+ Spacing = new Vector2(0, ITEM_SPACING),
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
@@ -63,14 +65,14 @@ namespace osu.Game.Overlays.Settings
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
AddRangeInternal(new Drawable[]
{
new Box
{
Name = "separator",
- Colour = new Color4(0, 0, 0, 255),
+ Colour = colourProvider.Background6,
RelativeSizeAxes = Axes.X,
Height = border_size,
},
@@ -78,8 +80,8 @@ namespace osu.Game.Overlays.Settings
{
Padding = new MarginPadding
{
- Top = margin + border_size,
- Bottom = margin + 10,
+ Top = 28,
+ Bottom = 40,
},
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -87,13 +89,11 @@ namespace osu.Game.Overlays.Settings
{
header = new OsuSpriteText
{
- Font = OsuFont.GetFont(size: header_size),
+ Font = OsuFont.TorusAlternate.With(size: header_size),
Text = Header,
- Colour = colours.Yellow,
Margin = new MarginPadding
{
- Left = SettingsPanel.CONTENT_MARGINS,
- Right = SettingsPanel.CONTENT_MARGINS
+ Horizontal = SettingsPanel.CONTENT_MARGINS
}
},
FlowContent
diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs
index 4aa9360452..c2cf08ac98 100644
--- a/osu.Game/Overlays/Settings/SettingsSubsection.cs
+++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs
@@ -46,13 +46,17 @@ namespace osu.Game.Overlays.Settings
FlowContent = new FillFlowContainer
{
+ Margin = new MarginPadding { Top = SettingsSection.ITEM_SPACING },
Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 8),
+ Spacing = new Vector2(0, SettingsSection.ITEM_SPACING),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
}
+ private const int header_height = 43;
+ private const int header_font_size = 20;
+
[BackgroundDependencyLoader]
private void load()
{
@@ -60,9 +64,9 @@ namespace osu.Game.Overlays.Settings
{
new OsuSpriteText
{
- Text = Header.ToString().ToUpper(), // TODO: Add localisation support after https://github.com/ppy/osu-framework/pull/4603 is merged.
- Margin = new MarginPadding { Vertical = 30, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS },
- Font = OsuFont.GetFont(weight: FontWeight.Bold),
+ Text = Header,
+ Margin = new MarginPadding { Vertical = (header_height - header_font_size) * 0.5f, Horizontal = SettingsPanel.CONTENT_MARGINS },
+ Font = OsuFont.GetFont(size: header_font_size),
},
FlowContent
});
diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs
index d28dbf1068..68562802cf 100644
--- a/osu.Game/Overlays/Settings/SettingsTextBox.cs
+++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Overlays.Settings
@@ -13,5 +15,17 @@ namespace osu.Game.Overlays.Settings
RelativeSizeAxes = Axes.X,
CommitOnFocusLost = true
};
+
+ public override Bindable Current
+ {
+ get => base.Current;
+ set
+ {
+ if (value.Default == null)
+ throw new InvalidOperationException($"Bindable settings of type {nameof(Bindable)} should have a non-null default value.");
+
+ base.Current = value;
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs
index 4ca6e2ec42..93b1b19b17 100644
--- a/osu.Game/Overlays/Settings/Sidebar.cs
+++ b/osu.Game/Overlays/Settings/Sidebar.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -15,22 +16,23 @@ using osuTK;
namespace osu.Game.Overlays.Settings
{
- public class Sidebar : Container, IStateful
+ public class Sidebar : Container, IStateful
{
- private readonly FillFlowContainer content;
- public const float DEFAULT_WIDTH = Toolbar.Toolbar.HEIGHT * 1.4f;
+ private readonly Box background;
+ private readonly FillFlowContainer content;
+ public const float DEFAULT_WIDTH = 70;
public const int EXPANDED_WIDTH = 200;
public event Action StateChanged;
- protected override Container Content => content;
+ protected override Container Content => content;
public Sidebar()
{
RelativeSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
- new Box
+ background = new Box
{
Colour = OsuColour.Gray(0.02f),
RelativeSizeAxes = Axes.Both,
@@ -39,7 +41,7 @@ namespace osu.Game.Overlays.Settings
{
Children = new[]
{
- content = new FillFlowContainer
+ content = new FillFlowContainer
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
@@ -52,6 +54,12 @@ namespace osu.Game.Overlays.Settings
};
}
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ background.Colour = colourProvider.Background5;
+ }
+
private ScheduledDelegate expandEvent;
private ExpandedState state;
@@ -80,8 +88,6 @@ namespace osu.Game.Overlays.Settings
{
public SidebarScrollContainer()
{
- Content.Anchor = Anchor.CentreLeft;
- Content.Origin = Anchor.CentreLeft;
RelativeSizeAxes = Axes.Both;
ScrollbarVisible = false;
}
diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs
index cf6a313a1f..1a34143e1f 100644
--- a/osu.Game/Overlays/Settings/SidebarButton.cs
+++ b/osu.Game/Overlays/Settings/SidebarButton.cs
@@ -1,110 +1,40 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osuTK;
-using osuTK.Graphics;
using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
+using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings
{
- public class SidebarButton : OsuButton
+ public abstract class SidebarButton : OsuButton
{
- private readonly ConstrainedIconContainer iconContainer;
- private readonly SpriteText headerText;
- private readonly Box selectionIndicator;
- private readonly Container text;
+ protected const double FADE_DURATION = 500;
- // always consider as part of flow, even when not visible (for the sake of the initial animation).
- public override bool IsPresent => true;
-
- private SettingsSection section;
-
- public SettingsSection Section
- {
- get => section;
- set
- {
- section = value;
- headerText.Text = value.Header;
- iconContainer.Icon = value.CreateIcon();
- }
- }
-
- private bool selected;
-
- public bool Selected
- {
- get => selected;
- set
- {
- selected = value;
-
- if (selected)
- {
- selectionIndicator.FadeIn(50);
- text.FadeColour(Color4.White, 50);
- }
- else
- {
- selectionIndicator.FadeOut(50);
- text.FadeColour(OsuColour.Gray(0.6f), 50);
- }
- }
- }
-
- public SidebarButton()
- {
- Height = Sidebar.DEFAULT_WIDTH;
- RelativeSizeAxes = Axes.X;
-
- BackgroundColour = Color4.Black;
-
- AddRange(new Drawable[]
- {
- text = new Container
- {
- Width = Sidebar.DEFAULT_WIDTH,
- RelativeSizeAxes = Axes.Y,
- Colour = OsuColour.Gray(0.6f),
- Children = new Drawable[]
- {
- headerText = new OsuSpriteText
- {
- Position = new Vector2(Sidebar.DEFAULT_WIDTH + 10, 0),
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- },
- iconContainer = new ConstrainedIconContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(20),
- },
- }
- },
- selectionIndicator = new Box
- {
- Alpha = 0,
- RelativeSizeAxes = Axes.Y,
- Width = 5,
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- },
- });
- }
+ [Resolved]
+ protected OverlayColourProvider ColourProvider { get; private set; }
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load()
{
- selectionIndicator.Colour = colours.Yellow;
+ BackgroundColour = ColourProvider.Background5;
}
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ UpdateState();
+ FinishTransforms(true);
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ UpdateState();
+ return false;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e) => UpdateState();
+
+ protected abstract void UpdateState();
}
}
diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs
new file mode 100644
index 0000000000..fd57996b1b
--- /dev/null
+++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs
@@ -0,0 +1,130 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osuTK;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.Containers;
+
+namespace osu.Game.Overlays.Settings
+{
+ public class SidebarIconButton : SidebarButton
+ {
+ private const float selection_indicator_height_active = 18;
+ private const float selection_indicator_height_inactive = 4;
+
+ private readonly ConstrainedIconContainer iconContainer;
+ private readonly SpriteText headerText;
+ private readonly CircularContainer selectionIndicator;
+ private readonly Container textIconContent;
+
+ // always consider as part of flow, even when not visible (for the sake of the initial animation).
+ public override bool IsPresent => true;
+
+ private SettingsSection section;
+
+ public SettingsSection Section
+ {
+ get => section;
+ set
+ {
+ section = value;
+ headerText.Text = value.Header;
+ iconContainer.Icon = value.CreateIcon();
+ }
+ }
+
+ private bool selected;
+
+ public bool Selected
+ {
+ get => selected;
+ set
+ {
+ selected = value;
+
+ if (IsLoaded)
+ UpdateState();
+ }
+ }
+
+ public SidebarIconButton()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 46;
+
+ AddRange(new Drawable[]
+ {
+ textIconContent = new Container
+ {
+ Width = Sidebar.DEFAULT_WIDTH,
+ RelativeSizeAxes = Axes.Y,
+ Colour = OsuColour.Gray(0.6f),
+ Children = new Drawable[]
+ {
+ headerText = new OsuSpriteText
+ {
+ Position = new Vector2(Sidebar.DEFAULT_WIDTH + 10, 0),
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ iconContainer = new ConstrainedIconContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(20),
+ },
+ }
+ },
+ selectionIndicator = new CircularContainer
+ {
+ Alpha = 0,
+ Width = 4,
+ Height = selection_indicator_height_inactive,
+ Masking = true,
+ CornerRadius = 1.5f,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Margin = new MarginPadding
+ {
+ Left = 9,
+ },
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.White
+ }
+ },
+ });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ selectionIndicator.Colour = ColourProvider.Highlight1;
+ }
+
+ protected override void UpdateState()
+ {
+ if (Selected)
+ {
+ textIconContent.FadeColour(ColourProvider.Content1, FADE_DURATION, Easing.OutQuint);
+
+ selectionIndicator.FadeIn(FADE_DURATION, Easing.OutQuint);
+ selectionIndicator.ResizeHeightTo(selection_indicator_height_active, FADE_DURATION, Easing.OutElasticHalf);
+ }
+ else
+ {
+ textIconContent.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint);
+
+ selectionIndicator.FadeOut(FADE_DURATION, Easing.OutQuint);
+ selectionIndicator.ResizeHeightTo(selection_indicator_height_inactive, FADE_DURATION, Easing.OutQuint);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs
index 55e8aee266..c84cba8189 100644
--- a/osu.Game/Overlays/SettingsOverlay.cs
+++ b/osu.Game/Overlays/SettingsOverlay.cs
@@ -24,12 +24,13 @@ namespace osu.Game.Overlays
protected override IEnumerable CreateSections() => new SettingsSection[]
{
new GeneralSection(),
- new GraphicsSection(),
- new AudioSection(),
+ new SkinSection(),
new InputSection(createSubPanel(new KeyBindingPanel())),
new UserInterfaceSection(),
new GameplaySection(),
- new SkinSection(),
+ new RulesetSection(),
+ new AudioSection(),
+ new GraphicsSection(),
new OnlineSection(),
new MaintenanceSection(),
new DebugSection(),
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index bda4bb5ece..0ceb7fc50d 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osuTK;
-using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -15,7 +14,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
@@ -25,7 +23,7 @@ namespace osu.Game.Overlays
[Cached]
public abstract class SettingsPanel : OsuFocusedOverlayContainer
{
- public const float CONTENT_MARGINS = 15;
+ public const float CONTENT_MARGINS = 20;
public const float TRANSITION_LENGTH = 600;
@@ -46,7 +44,7 @@ namespace osu.Game.Overlays
protected override Container Content => ContentContainer;
protected Sidebar Sidebar;
- private SidebarButton selectedSidebarButton;
+ private SidebarIconButton selectedSidebarButton;
public SettingsSectionsContainer SectionsContainer { get; private set; }
@@ -64,6 +62,9 @@ namespace osu.Game.Overlays
public IBindable CurrentSection = new Bindable();
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
protected SettingsPanel(bool showSidebar)
{
this.showSidebar = showSidebar;
@@ -89,7 +90,7 @@ namespace osu.Game.Overlays
Origin = Anchor.TopRight,
Scale = new Vector2(2, 1), // over-extend to the left for transitions
RelativeSizeAxes = Axes.Both,
- Colour = OsuColour.Gray(0.05f),
+ Colour = colourProvider.Background4,
Alpha = 1,
},
loading = new LoadingLayer
@@ -105,17 +106,23 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
ExpandableHeader = CreateHeader(),
SelectedSection = { BindTarget = CurrentSection },
- FixedHeader = searchTextBox = new SeekLimitedSearchTextBox
+ FixedHeader = new Container
{
RelativeSizeAxes = Axes.X,
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- Width = 0.95f,
- Margin = new MarginPadding
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding
{
- Top = 20,
- Bottom = 20
+ Vertical = 20,
+ Horizontal = CONTENT_MARGINS
},
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Child = searchTextBox = new SeekLimitedSearchTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ }
},
Footer = CreateFooter().With(f => f.Alpha = 0)
});
@@ -245,11 +252,11 @@ namespace osu.Game.Overlays
});
}
- private IEnumerable createSidebarButtons()
+ private IEnumerable createSidebarButtons()
{
foreach (var section in SectionsContainer)
{
- yield return new SidebarButton
+ yield return new SidebarIconButton
{
Section = section,
Action = () =>
@@ -292,11 +299,12 @@ namespace osu.Game.Overlays
Direction = FillDirection.Vertical,
};
- public SettingsSectionsContainer()
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
{
HeaderBackground = new Box
{
- Colour = Color4.Black,
+ Colour = colourProvider.Background4,
RelativeSizeAxes = Axes.Both
};
}
diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs
index 1fa233d9d4..a65d792a9f 100644
--- a/osu.Game/Overlays/SettingsSubPanel.cs
+++ b/osu.Game/Overlays/SettingsSubPanel.cs
@@ -7,10 +7,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Overlays
{
@@ -34,18 +32,18 @@ namespace osu.Game.Overlays
protected override bool DimMainContent => false; // dimming is handled by main overlay
- private class BackButton : OsuButton
+ private class BackButton : SidebarButton
{
+ private Container content;
+
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(Sidebar.DEFAULT_WIDTH);
- BackgroundColour = Color4.Black;
-
AddRange(new Drawable[]
{
- new Container
+ content = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -71,6 +69,11 @@ namespace osu.Game.Overlays
}
});
}
+
+ protected override void UpdateState()
+ {
+ content.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint);
+ }
}
}
}
diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs
index 73bab31e82..d8babf2f32 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
///
/// The current strain level.
///
- protected double CurrentStrain { get; private set; } = 1;
+ protected double CurrentStrain { get; private set; }
protected StrainDecaySkill(Mod[] mods)
: base(mods)
diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
index 0880f1b08e..bbd2f079aa 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
///
protected virtual int SectionLength => 400;
- private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
+ private double currentSectionPeak; // We also keep track of the peak strain level in the current section.
private double currentSectionEnd;
diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index e9865f6c8b..c0b339a231 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -55,7 +55,10 @@ namespace osu.Game.Rulesets.UI
///
/// The current direction of playback to be exposed to frame stable children.
///
- private int direction;
+ ///
+ /// Initially it is presumed that playback will proceed in the forward direction.
+ ///
+ private int direction = 1;
[BackgroundDependencyLoader(true)]
private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler)
@@ -139,7 +142,9 @@ namespace osu.Game.Rulesets.UI
state = PlaybackState.NotValid;
}
- if (state == PlaybackState.Valid)
+ // if the proposed time is the same as the current time, assume that the clock will continue progressing in the same direction as previously.
+ // this avoids spurious flips in direction from -1 to 1 during rewinds.
+ if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime)
direction = proposedTime >= manualClock.CurrentTime ? 1 : -1;
double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime);
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index dde956233b..cf22a8fda4 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -72,9 +72,12 @@ namespace osu.Game.Scoring
}
}
- // We're calling .Result, but this should not be a blocking call due to the above GetDifficultyAsync() calls.
- return scores.OrderByDescending(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken).Result)
- .ThenBy(s => s.OnlineScoreID)
+ var totalScores = await Task.WhenAll(scores.Select(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken))).ConfigureAwait(false);
+
+ return scores.Select((score, index) => (score, totalScore: totalScores[index]))
+ .OrderByDescending(g => g.totalScore)
+ .ThenBy(g => g.score.OnlineScoreID)
+ .Select(g => g.score)
.ToArray();
}
diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs
index e250791b72..ea158c5789 100644
--- a/osu.Game/Screens/Play/FailAnimation.cs
+++ b/osu.Game/Screens/Play/FailAnimation.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play
{
///
/// Manage the animation to be applied when a player fails.
- /// Single file; automatically disposed after use.
+ /// Single use and automatically disposed after use.
///
public class FailAnimation : CompositeDrawable
{
diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs
index 63de5c8de5..87b19e8433 100644
--- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs
@@ -11,7 +11,6 @@ namespace osu.Game.Screens.Play.HUD
public class DefaultScoreCounter : GameplayScoreCounter, ISkinnableDrawable
{
public DefaultScoreCounter()
- : base(6)
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
diff --git a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs
index e09630d2c4..e05eff5f3e 100644
--- a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs
@@ -14,8 +14,8 @@ namespace osu.Game.Screens.Play.HUD
{
private Bindable scoreDisplayMode;
- protected GameplayScoreCounter(int leading = 0, bool useCommaSeparator = false)
- : base(leading, useCommaSeparator)
+ protected GameplayScoreCounter()
+ : base(6)
{
}
diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
index d04e60a2ab..b1c07512dd 100644
--- a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
@@ -4,11 +4,9 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -148,7 +146,7 @@ namespace osu.Game.Screens.Play.HUD
Score2Text.X = Math.Max(5 + Score2Text.DrawWidth / 2, score2Bar.DrawWidth);
}
- protected class MatchScoreCounter : ScoreCounter
+ protected class MatchScoreCounter : CommaSeparatedScoreCounter
{
private OsuSpriteText displayedSpriteText;
@@ -173,8 +171,6 @@ namespace osu.Game.Screens.Play.HUD
=> displayedSpriteText.Font = winning
? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true);
-
- protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N0");
}
}
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 090210e611..444bea049b 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -947,7 +947,7 @@ namespace osu.Game.Screens.Play
public override void OnSuspending(IScreen next)
{
- screenSuspension?.Expire();
+ screenSuspension?.RemoveAndDisposeImmediately();
fadeOut();
base.OnSuspending(next);
@@ -955,7 +955,8 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(IScreen next)
{
- screenSuspension?.Expire();
+ screenSuspension?.RemoveAndDisposeImmediately();
+ failAnimation?.RemoveAndDisposeImmediately();
// if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
if (prepareScoreForDisplayTask == null)
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 94a61a4ef3..cf5bff57cf 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play
{
protected const float BACKGROUND_BLUR = 15;
+ private const double content_out_duration = 300;
+
public override bool HideOverlaysOnEnter => hideOverlays;
public override bool DisallowExternalBeatmapRulesetChanges => true;
@@ -135,36 +137,39 @@ namespace osu.Game.Screens.Play
muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce);
- InternalChild = (content = new LogoTrackingContainer
+ InternalChildren = new Drawable[]
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- }).WithChildren(new Drawable[]
- {
- MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade)
+ (content = new LogoTrackingContainer
{
- Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- },
- PlayerSettings = new FillFlowContainer
+ RelativeSizeAxes = Axes.Both,
+ }).WithChildren(new Drawable[]
{
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 20),
- Margin = new MarginPadding(25),
- Children = new PlayerSettingsGroup[]
+ MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade)
{
- VisualSettings = new VisualSettings(),
- new InputSettings()
- }
- },
- idleTracker = new IdleTracker(750),
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ PlayerSettings = new FillFlowContainer
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 20),
+ Margin = new MarginPadding(25),
+ Children = new PlayerSettingsGroup[]
+ {
+ VisualSettings = new VisualSettings(),
+ new InputSettings()
+ }
+ },
+ idleTracker = new IdleTracker(750),
+ }),
lowPassFilter = new AudioFilter(audio.TrackMixer)
- });
+ };
if (Beatmap.Value.BeatmapInfo.EpilepsyWarning)
{
@@ -195,7 +200,6 @@ namespace osu.Game.Screens.Play
epilepsyWarning.DimmableBackground = b;
});
- lowPassFilter.CutoffTo(500, 100, Easing.OutCubic);
Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
content.ScaleTo(0.7f);
@@ -240,15 +244,15 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(IScreen next)
{
cancelLoad();
+ contentOut();
- content.ScaleTo(0.7f, 150, Easing.InQuint);
- this.FadeOut(150);
+ // Ensure the screen doesn't expire until all the outwards fade operations have completed.
+ this.Delay(content_out_duration).FadeOut();
ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
BackgroundBrightnessReduction = false;
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
- lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
return base.OnExiting(next);
}
@@ -344,6 +348,7 @@ namespace osu.Game.Screens.Play
content.FadeInFromZero(400);
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
+ lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint));
}
@@ -353,8 +358,9 @@ namespace osu.Game.Screens.Play
// Ensure the logo is no longer tracking before we scale the content
content.StopTracking();
- content.ScaleTo(0.7f, 300, Easing.InQuint);
- content.FadeOut(250);
+ content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint);
+ content.FadeOut(content_out_duration, Easing.OutQuint);
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration);
}
private void pushWhenLoaded()
@@ -381,7 +387,7 @@ namespace osu.Game.Screens.Play
contentOut();
- TransformSequence pushSequence = this.Delay(250);
+ TransformSequence pushSequence = this.Delay(content_out_duration);
// only show if the warning was created (i.e. the beatmap needs it)
// and this is not a restart of the map (the warning expires after first load).
@@ -400,6 +406,11 @@ namespace osu.Game.Screens.Play
})
.Delay(EpilepsyWarning.FADE_DURATION);
}
+ else
+ {
+ // This goes hand-in-hand with the restoration of low pass filter in contentOut().
+ this.TransformBindableTo(volumeAdjustment, 0, content_out_duration, Easing.OutCubic);
+ }
pushSequence.Schedule(() =>
{
diff --git a/osu.Game/Skinning/ISkinSource.cs b/osu.Game/Skinning/ISkinSource.cs
index ba3e2bf6ad..a5ed0fc990 100644
--- a/osu.Game/Skinning/ISkinSource.cs
+++ b/osu.Game/Skinning/ISkinSource.cs
@@ -12,6 +12,9 @@ namespace osu.Game.Skinning
///
public interface ISkinSource : ISkin
{
+ ///
+ /// Fired whenever a source change occurs, signalling that consumers should re-query as required.
+ ///
event Action SourceChanged;
///
diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs
index a12defe87e..0c9a82074f 100644
--- a/osu.Game/Skinning/LegacyScoreCounter.cs
+++ b/osu.Game/Skinning/LegacyScoreCounter.cs
@@ -16,7 +16,6 @@ namespace osu.Game.Skinning
public bool UsesFixedAnchor { get; set; }
public LegacyScoreCounter()
- : base(6)
{
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
index f5a7788359..b884794739 100644
--- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
+++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
@@ -58,10 +58,8 @@ namespace osu.Game.Skinning
return base.CreateChildDependencies(parent);
}
- protected override void OnSourceChanged()
+ protected override void RefreshSources()
{
- ResetSources();
-
// Populate a local list first so we can adjust the returned order as we go.
var sources = new List();
@@ -91,8 +89,7 @@ namespace osu.Game.Skinning
else
sources.Add(rulesetResourcesSkin);
- foreach (var skin in sources)
- AddSource(skin);
+ SetSources(sources);
}
protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin)
diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs
index ada6e4b788..c8e4c2c7b6 100644
--- a/osu.Game/Skinning/SkinProvidingContainer.cs
+++ b/osu.Game/Skinning/SkinProvidingContainer.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@@ -40,10 +41,12 @@ namespace osu.Game.Skinning
protected virtual bool AllowColourLookup => true;
+ private readonly object sourceSetLock = new object();
+
///
/// A dictionary mapping each source to a wrapper which handles lookup allowances.
///
- private readonly List<(ISkin skin, DisableableSkinSource wrapped)> skinSources = new List<(ISkin, DisableableSkinSource)>();
+ private (ISkin skin, DisableableSkinSource wrapped)[] skinSources = Array.Empty<(ISkin skin, DisableableSkinSource wrapped)>();
///
/// Constructs a new initialised with a single skin source.
@@ -52,7 +55,7 @@ namespace osu.Game.Skinning
: this()
{
if (skin != null)
- AddSource(skin);
+ SetSources(new[] { skin });
}
///
@@ -168,49 +171,42 @@ namespace osu.Game.Skinning
}
///
- /// Add a new skin to this provider. Will be added to the end of the lookup order precedence.
+ /// Replace the sources used for lookups in this container.
///
- /// The skin to add.
- protected void AddSource(ISkin skin)
+ ///
+ /// This does not implicitly fire a event. Consider calling if required.
+ ///
+ /// The new sources.
+ protected void SetSources(IEnumerable sources)
{
- skinSources.Add((skin, new DisableableSkinSource(skin, this)));
+ lock (sourceSetLock)
+ {
+ foreach (var skin in skinSources)
+ {
+ if (skin.skin is ISkinSource source)
+ source.SourceChanged -= TriggerSourceChanged;
+ }
- if (skin is ISkinSource source)
- source.SourceChanged += TriggerSourceChanged;
+ skinSources = sources.Select(skin => (skin, new DisableableSkinSource(skin, this))).ToArray();
+
+ foreach (var skin in skinSources)
+ {
+ if (skin.skin is ISkinSource source)
+ source.SourceChanged += TriggerSourceChanged;
+ }
+ }
}
///
- /// Remove a skin from this provider.
- ///
- /// The skin to remove.
- protected void RemoveSource(ISkin skin)
- {
- if (skinSources.RemoveAll(s => s.skin == skin) == 0)
- return;
-
- if (skin is ISkinSource source)
- source.SourceChanged -= TriggerSourceChanged;
- }
-
- ///
- /// Clears all skin sources.
- ///
- protected void ResetSources()
- {
- foreach (var i in skinSources.ToArray())
- RemoveSource(i.skin);
- }
-
- ///
- /// Invoked when any source has changed (either or a source registered via ).
+ /// Invoked after any consumed source change, before the external event is fired.
/// This is also invoked once initially during to ensure sources are ready for children consumption.
///
- protected virtual void OnSourceChanged() { }
+ protected virtual void RefreshSources() { }
protected void TriggerSourceChanged()
{
// Expose to implementations, giving them a chance to react before notifying external consumers.
- OnSourceChanged();
+ RefreshSources();
SourceChanged?.Invoke();
}
diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs
index e7125bb034..20c2fcc075 100644
--- a/osu.Game/Skinning/SkinnableTargetContainer.cs
+++ b/osu.Game/Skinning/SkinnableTargetContainer.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using System.Threading;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -22,6 +23,8 @@ namespace osu.Game.Skinning
public bool ComponentsLoaded { get; private set; }
+ private CancellationTokenSource cancellationSource;
+
public SkinnableTargetContainer(SkinnableTarget target)
{
Target = target;
@@ -38,6 +41,9 @@ namespace osu.Game.Skinning
content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer;
+ cancellationSource?.Cancel();
+ cancellationSource = null;
+
if (content != null)
{
LoadComponentAsync(content, wrapper =>
@@ -45,7 +51,7 @@ namespace osu.Game.Skinning
AddInternal(wrapper);
components.AddRange(wrapper.Children.OfType());
ComponentsLoaded = true;
- });
+ }, (cancellationSource = new CancellationTokenSource()).Token);
}
else
ComponentsLoaded = true;
diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs
new file mode 100644
index 0000000000..f7b7471634
--- /dev/null
+++ b/osu.Game/Stores/RealmFileStore.cs
@@ -0,0 +1,116 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.IO;
+using System.Linq;
+using osu.Framework.Extensions;
+using osu.Framework.IO.Stores;
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Framework.Testing;
+using osu.Game.Database;
+using osu.Game.Models;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Stores
+{
+ ///
+ /// Handles the storing of files to the file system (and database) backing.
+ ///
+ [ExcludeFromDynamicCompile]
+ public class RealmFileStore
+ {
+ private readonly RealmContextFactory realmFactory;
+
+ public readonly IResourceStore Store;
+
+ public readonly Storage Storage;
+
+ public RealmFileStore(RealmContextFactory realmFactory, Storage storage)
+ {
+ this.realmFactory = realmFactory;
+
+ Storage = storage.GetStorageForDirectory(@"files");
+ Store = new StorageBackedResourceStore(Storage);
+ }
+
+ ///
+ /// Add a new file to the game-wide database, copying it to permanent storage if not already present.
+ ///
+ /// The file data stream.
+ /// The realm instance to add to. Should already be in a transaction.
+ ///
+ public RealmFile Add(Stream data, Realm realm)
+ {
+ string hash = data.ComputeSHA2Hash();
+
+ var existing = realm.Find(hash);
+
+ var file = existing ?? new RealmFile { Hash = hash };
+
+ if (!checkFileExistsAndMatchesHash(file))
+ copyToStore(file, data);
+
+ if (!file.IsManaged)
+ realm.Add(file);
+
+ return file;
+ }
+
+ private void copyToStore(RealmFile file, Stream data)
+ {
+ data.Seek(0, SeekOrigin.Begin);
+
+ using (var output = Storage.GetStream(file.StoragePath, FileAccess.Write))
+ data.CopyTo(output);
+
+ data.Seek(0, SeekOrigin.Begin);
+ }
+
+ private bool checkFileExistsAndMatchesHash(RealmFile file)
+ {
+ string path = file.StoragePath;
+
+ // we may be re-adding a file to fix missing store entries.
+ if (!Storage.Exists(path))
+ return false;
+
+ // even if the file already exists, check the existing checksum for safety.
+ using (var stream = Storage.GetStream(path))
+ return stream.ComputeSHA2Hash() == file.Hash;
+ }
+
+ public void Cleanup()
+ {
+ var realm = realmFactory.Context;
+
+ // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal.
+ using (var transaction = realm.BeginWrite())
+ {
+ // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707)
+ var files = realm.All().ToList();
+
+ foreach (var file in files)
+ {
+ if (file.BacklinksCount > 0)
+ continue;
+
+ try
+ {
+ Storage.Delete(file.StoragePath);
+ realm.Remove(file);
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, $@"Could not delete databased file {file.Hash}");
+ }
+ }
+
+ transaction.Commit();
+ }
+ }
+ }
+}
diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs
index 77db697cb6..6a11bd3fea 100644
--- a/osu.Game/Tests/Visual/OsuGameTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs
@@ -78,9 +78,11 @@ namespace osu.Game.Tests.Visual
protected void CreateGame()
{
- AddGame(Game = new TestOsuGame(LocalStorage, API));
+ AddGame(Game = CreateTestGame());
}
+ protected virtual TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API);
+
protected void PushAndConfirm(Func newScreen)
{
Screen screen = null;
@@ -135,7 +137,8 @@ namespace osu.Game.Tests.Visual
public new void PerformFromScreen(Action action, IEnumerable validScreens = null) => base.PerformFromScreen(action, validScreens);
- public TestOsuGame(Storage storage, IAPIProvider api)
+ public TestOsuGame(Storage storage, IAPIProvider api, string[] args = null)
+ : base(args)
{
Storage = storage;
API = api;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 4877ddf725..184c9d3f63 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index edce9d27fe..38b920420b 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -93,7 +93,7 @@
-
+