Merge branch 'master' into no-more-difficulty-control-points-info

This commit is contained in:
Dan Balasescu 2021-10-13 20:17:57 +09:00 committed by GitHub
commit e49f1f6e6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
124 changed files with 2626 additions and 740 deletions

View File

@ -79,9 +79,14 @@ jobs:
run: | run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround. # 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 # FIXME: Suppress warnings from templates project
dotnet codefilesanity | while read -r line; do exit_code=0
echo "::warning::$line" while read -r line; do
done 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. # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run) # - name: .NET Format (Dry Run)

View File

@ -52,7 +52,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1004.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.1012.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; 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) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(5.169743871843191d, "diffcalc-test")] [TestCase(5.1696411260785498d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new CatchModDoubleTime()); => Test(expected, name, new CatchModDoubleTime());

View File

@ -47,8 +47,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{ {
StarRating = skills[0].DifficultyValue() * star_scaling_factor, StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods, Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate),
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
ScoreMultiplier = getScoreMultiplier(mods), ScoreMultiplier = getScoreMultiplier(mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills Skills = skills

View File

@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6634445062299665d, "diffcalc-test")] [TestCase(6.5867229481955389d, "diffcalc-test")]
[TestCase(1.0414203870195022d, "zero-length-sliders")] [TestCase(1.0416315570967911d, "zero-length-sliders")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(8.3858089051603368d, "diffcalc-test")] [TestCase(8.2730989071947896d, "diffcalc-test")]
[TestCase(1.2723279173428435d, "zero-length-sliders")] [TestCase(1.2726413186221039d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime()); => Test(expected, name, new OsuModDoubleTime());

View File

@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public class TestSceneSpinnerRotation : TestSceneOsuPlayer public class TestSceneSpinnerRotation : TestSceneOsuPlayer
{ {
private const double spinner_start_time = 100;
private const double spinner_duration = 6000;
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; }
@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
double finalTrackerRotation = 0, trackerRotationTolerance = 0; double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0; double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(5000); addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () => AddStep("retrieve disc rotation", () =>
{ {
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation; finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation); AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
addSeekStep(2500); addSeekStep(spinner_start_time + 2500);
AddAssert("disc rotation rewound", 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. // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
// due to the exponential damping applied we're allowing a larger margin of error of about 10% // due to the exponential damping applied we're allowing a larger margin of error of about 10%
@ -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. // 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)); () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000); addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same", AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance)); () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same", AddAssert("is symbol rotation almost same",
@ -140,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test] [Test]
public void TestSpinnerNormalBonusRewinding() public void TestSpinnerNormalBonusRewinding()
{ {
addSeekStep(1000); addSeekStep(spinner_start_time + 1000);
AddAssert("player score matching expected bonus score", () => 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)); 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<OsuReplayFrame>()
.Select(replayFrame =>
{
var adjustedTime = replayFrame.Time * rate;
return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
})
.Cast<ReplayFrame>()
.ToList()
};
private void addSeekStep(double time) private void addSeekStep(double time)
{ {
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
} }
@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests
new Spinner new Spinner
{ {
Position = new Vector2(256, 192), Position = new Vector2(256, 192),
EndTime = 6000, StartTime = spinner_start_time,
Duration = spinner_duration
}, },
} }
}; };

View File

@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double FlashlightRating { get; set; } public double FlashlightRating { get; set; }
public double ApproachRate { get; set; } public double ApproachRate { get; set; }
public double OverallDifficulty { get; set; } public double OverallDifficulty { get; set; }
public double DrainRate { get; set; }
public int HitCircleCount { get; set; } public int HitCircleCount { get; set; }
public int SpinnerCount { get; set; } public int SpinnerCount { get; set; }
} }

View File

@ -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 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; 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) // 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, FlashlightRating = flashlightRating,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6, OverallDifficulty = (80 - hitWindowGreat) / 6,
DrainRate = drainRate,
MaxCombo = maxCombo, MaxCombo = maxCombo,
HitCircleCount = hitCirclesCount, HitCircleCount = hitCirclesCount,
SpinnerCount = spinnerCount, SpinnerCount = spinnerCount,
@ -100,8 +102,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
HitWindows hitWindows = new OsuHitWindows(); HitWindows hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); 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 = hitWindows.WindowFor(HitResult.Great) / clockRate;
hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
return new Skill[] return new Skill[]
{ {

View File

@ -40,9 +40,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); 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. 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)) if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss); 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; 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(m => m is OsuModBlinds))
if (mods.Any(h => h is OsuModHidden)) 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 *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
}
aimValue *= approachRateBonus; aimValue *= approachRateBonus;
@ -153,11 +157,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; 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); speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
}
// Scale the speed value with accuracy and OD. // 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); 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. // Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); speedValue *= Math.Pow(0.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. // 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)); 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; accuracyValue *= 1.08;
if (mods.Any(m => m is OsuModFlashlight)) if (mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02; accuracyValue *= 1.02;

View File

@ -22,17 +22,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
} }
protected override double SkillMultiplier => 26.25; private double currentStrain = 1;
protected override double StrainDecayBase => 0.15;
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) if (current.BaseObject is Spinner)
return 0; return 0;
var osuCurrent = (OsuDifficultyHitObject)current; var osuCurrent = (OsuDifficultyHitObject)current;
double result = 0; double aimStrain = 0;
if (Previous.Count > 0) if (Previous.Count > 0)
{ {
@ -46,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
Math.Max(osuPrevious.JumpDistance - scale, 0) Math.Max(osuPrevious.JumpDistance - scale, 0)
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2) * Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
* Math.Max(osuCurrent.JumpDistance - scale, 0)); * 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); double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance);
return Math.Max( 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 (Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime
); );
} }
private double applyDiminishingExp(double val) => Math.Pow(val, 0.99); 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;
}
} }
} }

View File

@ -19,12 +19,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
} }
protected override double SkillMultiplier => 0.15; private double skillMultiplier => 0.15;
protected override double StrainDecayBase => 0.15; private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0; protected override double DecayWeight => 1.0;
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations. 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) if (current.BaseObject is Spinner)
return 0; return 0;
@ -62,5 +63,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return Math.Pow(smallDistNerf * result, 2.0); 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;
}
} }
} }

View File

@ -10,7 +10,7 @@ using osu.Framework.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
public abstract class OsuStrainSkill : StrainDecaySkill public abstract class OsuStrainSkill : StrainSkill
{ {
/// <summary> /// <summary>
/// The number of sections with the highest strains, which the peak strain reductions will apply to. /// The number of sections with the highest strains, which the peak strain reductions will apply to.

View File

@ -16,19 +16,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public class Speed : OsuStrainSkill public class Speed : OsuStrainSkill
{ {
private const double single_spacing_threshold = 125; private const double single_spacing_threshold = 125;
private const double rhythm_multiplier = 0.75;
private const double angle_bonus_begin = 5 * Math.PI / 6; private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
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 min_speed_bonus = 75; // ~200BPM private const double min_speed_bonus = 75; // ~200BPM
private const double speed_balancing_factor = 40; 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; private readonly double greatWindow;
public Speed(Mod[] mods, double hitWindowGreat) public Speed(Mod[] mods, double hitWindowGreat)
@ -37,52 +39,138 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
greatWindow = hitWindowGreat; greatWindow = hitWindowGreat;
} }
protected override double StrainValueOf(DifficultyHitObject current) /// <summary>
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
/// </summary>
private double calculateRhythmBonus(DifficultyHitObject current)
{ {
if (current.BaseObject is Spinner) if (current.BaseObject is Spinner)
return 0; return 0;
var osuCurrent = (OsuDifficultyHitObject)current; int previousIslandSize = 0;
var osuPrevious = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance); double rhythmComplexitySum = 0;
double strainTime = osuCurrent.StrainTime; 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 greatWindowFull = greatWindow * 2;
double speedWindowRatio = strainTime / greatWindowFull; double speedWindowRatio = strainTime / greatWindowFull;
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between) // Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
if (osuPrevious != null && strainTime < greatWindowFull && osuPrevious.StrainTime > strainTime) if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime)
strainTime = Interpolation.Lerp(osuPrevious.StrainTime, strainTime, speedWindowRatio); strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio);
// Cap deltatime to the OD 300 hitwindow. // 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. // 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); strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
// derive speedBonus for calculation
double speedBonus = 1.0; double speedBonus = 1.0;
if (strainTime < min_speed_bonus) 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) return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
{ }
angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57;
if (osuCurrent.Angle.Value < pi_over_2) private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
{
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);
}
}
return (1 + (speedBonus - 1) * 0.75) protected override double CalculateInitialStrain(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime);
* angleBonus
* (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) protected override double StrainValueAt(DifficultyHitObject current)
/ strainTime; {
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += strainValueOf(current) * skillMultiplier;
currentRhythm = calculateRhythmBonus(current);
return currentStrain * currentRhythm;
} }
} }
} }

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
[TestCase(2.2867022617692685d, "diffcalc-test")] [TestCase(2.2420075288523802d, "diffcalc-test")]
[TestCase(2.2867022617692685d, "diffcalc-test-strong")] [TestCase(2.2420075288523802d, "diffcalc-test-strong")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(3.1704781712282624d, "diffcalc-test")] [TestCase(3.134084469440479d, "diffcalc-test")]
[TestCase(3.1704781712282624d, "diffcalc-test-strong")] [TestCase(3.134084469440479d, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new TaikoModDoubleTime()); => Test(expected, name, new TaikoModDoubleTime());

View File

@ -47,10 +47,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) protected override Beatmap<TaikoHitObject> 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 // Rewrite the beatmap info to add the slider velocity multiplier
original.Difficulty = new TaikoMutliplierAppliedDifficulty(original.Difficulty); original.Difficulty = new TaikoMultiplierAppliedDifficulty(original.Difficulty);
} }
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken); Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
@ -191,15 +191,15 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap(); protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty private class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
{ {
public TaikoMutliplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty) public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
{ {
CopyFrom(difficulty); CopyFrom(difficulty);
} }
[UsedImplicitly] [UsedImplicitly]
public TaikoMutliplierAppliedDifficulty() public TaikoMultiplierAppliedDifficulty()
{ {
} }
@ -208,14 +208,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
public override void CopyTo(BeatmapDifficulty other) public override void CopyTo(BeatmapDifficulty other)
{ {
base.CopyTo(other); base.CopyTo(other);
if (!(other is TaikoMutliplierAppliedDifficulty)) if (!(other is TaikoMultiplierAppliedDifficulty))
SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
} }
public override void CopyFrom(IBeatmapDifficultyInfo other) public override void CopyFrom(IBeatmapDifficultyInfo other)
{ {
base.CopyFrom(other); base.CopyFrom(other);
if (!(other is TaikoMutliplierAppliedDifficulty)) if (!(other is TaikoMultiplierAppliedDifficulty))
SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
} }

View File

@ -94,8 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
StaminaStrain = staminaRating, StaminaStrain = staminaRating,
RhythmStrain = rhythmRating, RhythmStrain = rhythmRating,
ColourStrain = colourRating, ColourStrain = colourRating,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit), MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
Skills = skills Skills = skills
}; };

View File

@ -509,5 +509,17 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(LinkAction.External, result.Action); Assert.AreEqual(LinkAction.External, result.Action);
Assert.AreEqual("/relative", result.Argument); 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);
}
} }
} }

View File

@ -0,0 +1,114 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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<RealmFile>().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<RealmFile>().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<RealmFile>().Any());
Assert.True(files.Storage.Exists(path));
files.Cleanup();
Logger.Log($"Cleanup complete at {timer.ElapsedMilliseconds}");
Assert.True(realm.All<RealmFile>().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<RealmFile>().Any());
Assert.True(files.Storage.Exists(path));
files.Cleanup();
Assert.False(realm.All<RealmFile>().Any());
Assert.False(file.IsValid);
Assert.False(files.Storage.Exists(path));
});
}
}
}

View File

@ -1,3 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

@ -0,0 +1,213 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.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<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
ILive<IBeatmapInfo> iBeatmap = beatmap;
Assert.AreEqual(0, iBeatmap.Value.Length);
});
}
[Test]
public void TestValueAccessWithOpenContext()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? 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<RealmBeatmap>? 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<RealmBeatmap>? 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<RealmBeatmap>? 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<InvalidOperationException>(() =>
{
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<RealmBeatmap>().SubscribeForNotifications(gotChange);
RealmLive<RealmBeatmap>? 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<RealmBeatmap>().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<RealmBeatmap>().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<RealmBeatmap> sender, ChangeSet changes, Exception error)
{
changesTriggered++;
}
});
}
}
}

View File

@ -4,12 +4,13 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Nito.AsyncEx;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Models;
#nullable enable #nullable enable
@ -28,42 +29,109 @@ namespace osu.Game.Tests.Database
protected void RunTestWithRealm(Action<RealmContextFactory, Storage> testAction, [CallerMemberName] string caller = "") protected void RunTestWithRealm(Action<RealmContextFactory, Storage> testAction, [CallerMemberName] string caller = "")
{ {
AsyncContext.Run(() => using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{ {
var testStorage = storage.GetStorageForDirectory(caller); host.Run(new RealmTestGame(() =>
using (var realmFactory = new RealmContextFactory(testStorage, caller))
{ {
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); var testStorage = storage.GetStorageForDirectory(caller);
testAction(realmFactory, testStorage);
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.Dispose();
realmFactory.Compact();
Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
} realmFactory.Compact();
}); Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
}
}));
}
} }
protected void RunTestWithRealmAsync(Func<RealmContextFactory, Storage, Task> testAction, [CallerMemberName] string caller = "") protected void RunTestWithRealmAsync(Func<RealmContextFactory, Storage, Task> testAction, [CallerMemberName] string caller = "")
{ {
AsyncContext.Run(async () => using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{ {
var testStorage = storage.GetStorageForDirectory(caller); host.Run(new RealmTestGame(async () =>
using (var realmFactory = new RealmContextFactory(testStorage, caller))
{ {
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); var testStorage = storage.GetStorageForDirectory(caller);
await testAction(realmFactory, testStorage);
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.Dispose();
realmFactory.Compact();
Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); 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<Task> 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) private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory)

View File

@ -168,14 +168,14 @@ namespace osu.Game.Tests.Online
return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host); 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 internal class TestBeatmapModelDownloader : BeatmapModelDownloader
{ {
public TestBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost) public TestBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost)
: base(modelManager, apiProvider, gameHost) : base(modelManager, apiProvider, gameHost)
{ {
} }

View File

@ -6,7 +6,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
@ -65,10 +64,9 @@ namespace osu.Game.Tests.Skins
public new void TriggerSourceChanged() => base.TriggerSourceChanged(); public new void TriggerSourceChanged() => base.TriggerSourceChanged();
protected override void OnSourceChanged() protected override void RefreshSources()
{ {
ResetSources(); SetSources(sources);
sources.ForEach(AddSource);
} }
} }

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -18,16 +19,19 @@ namespace osu.Game.Tests.Visual.Audio
{ {
public class TestSceneAudioFilter : OsuTestScene public class TestSceneAudioFilter : OsuTestScene
{ {
private OsuSpriteText lowpassText; private OsuSpriteText lowPassText;
private AudioFilter lowpassFilter; private AudioFilter lowPassFilter;
private OsuSpriteText highpassText; private OsuSpriteText highPassText;
private AudioFilter highpassFilter; private AudioFilter highPassFilter;
private Track track; private Track track;
private WaveformTestBeatmap beatmap; private WaveformTestBeatmap beatmap;
private OsuSliderBar<int> lowPassSlider;
private OsuSliderBar<int> highPassSlider;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
@ -38,53 +42,89 @@ namespace osu.Game.Tests.Visual.Audio
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
lowpassFilter = new AudioFilter(audio.TrackMixer), lowPassFilter = new AudioFilter(audio.TrackMixer),
highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
lowpassText = new OsuSpriteText lowPassText = new OsuSpriteText
{ {
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz", Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40) Font = new FontUsage(size: 40)
}, },
new OsuSliderBar<int> lowPassSlider = new OsuSliderBar<int>
{ {
Width = 500, Width = 500,
Height = 50, Height = 50,
Padding = new MarginPadding(20), 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), Padding = new MarginPadding(20),
Text = $"High Pass: {highpassFilter.Cutoff.Value}hz", Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40) Font = new FontUsage(size: 40)
}, },
new OsuSliderBar<int> highPassSlider = new OsuSliderBar<int>
{ {
Width = 500, Width = 500,
Height = 50, Height = 50,
Padding = new MarginPadding(20), 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] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("Play Track", () => track.Start()); AddStep("Play Track", () => track.Start());
AddStep("Reset filters", () =>
{
lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF;
highPassFilter.Cutoff = 0;
});
waitTrackPlay(); waitTrackPlay();
} }
[Test] [Test]
public void TestLowPass() public void TestLowPassSweep()
{ {
AddStep("Filter Sweep", () => AddStep("Filter Sweep", () =>
{ {
lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic); .CutoffTo(0, 2000, Easing.OutCubic);
}); });
@ -92,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () => AddStep("Filter Sweep (reverse)", () =>
{ {
lowpassFilter.CutoffTo(0).Then() lowPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
}); });
@ -101,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio
} }
[Test] [Test]
public void TestHighPass() public void TestHighPassSweep()
{ {
AddStep("Filter Sweep", () => AddStep("Filter Sweep", () =>
{ {
highpassFilter.CutoffTo(0).Then() highPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
}); });
@ -113,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () => AddStep("Filter Sweep (reverse)", () =>
{ {
highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic); .CutoffTo(0, 2000, Easing.OutCubic);
}); });

View File

@ -32,6 +32,8 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for editor load", () => editor != null); 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("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); 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("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left)); AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left));
AddStep("Save and exit", () => AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
{
InputManager.Keys(PlatformAction.Save); AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
InputManager.Key(Key.Escape);
}); AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); 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); AddUntilStep("Wait for editor load", () => editor != null);
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7);
} }
} }
} }

View File

@ -103,6 +103,30 @@ namespace osu.Game.Tests.Visual.Gameplay
checkFrameCount(0); 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 const int max_frames_catchup = 50;
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () => 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) => private void checkFrameCount(int frames) =>
AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == 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 public class ClockConsumingChild : CompositeDrawable
{ {
private readonly OsuSpriteText text; private readonly OsuSpriteText text;

View File

@ -90,8 +90,12 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest(() => CreateTest(() =>
{ {
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true); 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("wait for fail", () => Player.HasFailed);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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<ImportProgressNotification>().Count() == 1);
}
}
}

View File

@ -32,12 +32,14 @@ namespace osu.Game.Tests.Visual.Playlists
private TestResultsScreen resultsScreen; private TestResultsScreen resultsScreen;
private int currentScoreId; private int currentScoreId;
private bool requestComplete; private bool requestComplete;
private int totalCount;
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
{ {
currentScoreId = 0; currentScoreId = 0;
requestComplete = false; requestComplete = false;
totalCount = 0;
bindHandler(); bindHandler();
}); });
@ -53,7 +55,6 @@ namespace osu.Game.Tests.Visual.Playlists
}); });
createResults(() => userScore); createResults(() => userScore);
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded); AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
} }
@ -62,7 +63,6 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestShowNullUserScore() public void TestShowNullUserScore()
{ {
createResults(); createResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
} }
@ -79,7 +79,6 @@ namespace osu.Game.Tests.Visual.Playlists
}); });
createResults(() => userScore); createResults(() => userScore);
waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded); AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().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)); AddStep("bind delayed handler", () => bindHandler(true));
createResults(); createResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
} }
@ -100,7 +98,6 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestFetchWhenScrolledToTheRight() public void TestFetchWhenScrolledToTheRight()
{ {
createResults(); createResults();
waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
@ -131,7 +128,6 @@ namespace osu.Game.Tests.Visual.Playlists
}); });
createResults(() => userScore); createResults(() => userScore);
waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
@ -161,13 +157,15 @@ namespace osu.Game.Tests.Visual.Playlists
})); }));
}); });
AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true); waitForDisplay();
} }
private void waitForDisplay() private void waitForDisplay()
{ {
AddUntilStep("wait for request to complete", () => requestComplete); AddUntilStep("wait for load to complete", () =>
AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true); requestComplete
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible);
AddWaitStep("wait for display", 5); AddWaitStep("wait for display", 5);
} }
@ -203,6 +201,7 @@ namespace osu.Game.Tests.Visual.Playlists
triggerFail(s); triggerFail(s);
else else
triggerSuccess(s, createUserResponse(userScore)); triggerSuccess(s, createUserResponse(userScore));
break; break;
case IndexPlaylistScoresRequest i: case IndexPlaylistScoresRequest i:
@ -248,6 +247,8 @@ namespace osu.Game.Tests.Visual.Playlists
} }
}; };
totalCount++;
for (int i = 1; i <= scores_per_result; i++) for (int i = 1; i <= scores_per_result; i++)
{ {
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
@ -285,6 +286,8 @@ namespace osu.Game.Tests.Visual.Playlists
}, },
Statistics = userScore.Statistics Statistics = userScore.Statistics
}); });
totalCount += 2;
} }
addCursor(multiplayerUserScore.ScoresAround.Lower); addCursor(multiplayerUserScore.ScoresAround.Lower);
@ -325,6 +328,8 @@ namespace osu.Game.Tests.Visual.Playlists
{ HitResult.Great, 300 } { HitResult.Great, 300 }
} }
}); });
totalCount++;
} }
addCursor(result); addCursor(result);

View File

@ -53,7 +53,11 @@ namespace osu.Game.Tests.Visual.Settings
}; };
[SettingSource("Sample string", "Change something for a mod")] [SettingSource("Sample string", "Change something for a mod")]
public Bindable<string> StringBindable { get; } = new Bindable<string>(); public Bindable<string> StringBindable { get; } = new Bindable<string>
{
Default = string.Empty,
Value = "Sample text"
};
} }
private enum TestEnum private enum TestEnum

View File

@ -3,6 +3,7 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Input.Handlers.Tablet;
@ -21,6 +22,9 @@ namespace osu.Game.Tests.Visual.Settings
private TestTabletHandler tabletHandler; private TestTabletHandler tabletHandler;
private TabletSettings settings; private TabletSettings settings;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {

View File

@ -142,6 +142,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("store selected beatmap", () => selected = Beatmap.Value); AddStep("store selected beatmap", () => selected = Beatmap.Value);
AddUntilStep("wait for beatmaps to load", () => songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>().Any());
AddStep("select next and enter", () => AddStep("select next and enter", () =>
{ {
InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>() InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
@ -599,10 +601,10 @@ namespace osu.Game.Tests.Visual.SongSelect
}); });
FilterableDifficultyIcon difficultyIcon = null; FilterableDifficultyIcon difficultyIcon = null;
AddStep("Find an icon", () => AddUntilStep("Find an icon", () =>
{ {
difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>() return (difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
.First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex()); .FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex())) != null;
}); });
AddStep("Click on a difficulty", () => AddStep("Click on a difficulty", () =>
@ -765,10 +767,10 @@ namespace osu.Game.Tests.Visual.SongSelect
}); });
FilterableGroupedDifficultyIcon groupIcon = null; FilterableGroupedDifficultyIcon groupIcon = null;
AddStep("Find group icon for different ruleset", () => AddUntilStep("Find group icon for different ruleset", () =>
{ {
groupIcon = set.ChildrenOfType<FilterableGroupedDifficultyIcon>() return (groupIcon = set.ChildrenOfType<FilterableGroupedDifficultyIcon>()
.First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3); .FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3)) != null;
}); });
AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0);

View File

@ -163,7 +163,6 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
AddUntilStep("wait for fetch", () => leaderboard.Scores != null); AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID)); 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() public void TestDeleteViaDatabase()
{ {
AddStep("delete top score", () => scoreManager.Delete(importedScores[0])); 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)); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
} }
} }

View File

@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod)); AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod)); AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod));
var mutlipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
AddStep(@"Add multiple Mods", () => changeMods(mutlipleIncrementMods)); AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(mutlipleIncrementMods)); AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods));
} }
[Test] [Test]

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.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 : () => { });
}
}
}

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Components
public DateTextBox() public DateTextBox()
{ {
base.Current = new Bindable<string>(); base.Current = new Bindable<string>(string.Empty);
((OsuTextBox)Control).OnCommit += (sender, newText) => ((OsuTextBox)Control).OnCommit += (sender, newText) =>
{ {

View File

@ -10,7 +10,7 @@ namespace osu.Game.Tournament.Models
{ {
public List<SeedingBeatmap> Beatmaps = new List<SeedingBeatmap>(); public List<SeedingBeatmap> Beatmaps = new List<SeedingBeatmap>();
public Bindable<string> Mod = new Bindable<string>(); public Bindable<string> Mod = new Bindable<string>(string.Empty);
public Bindable<int> Seed = new BindableInt public Bindable<int> Seed = new BindableInt
{ {

View File

@ -14,8 +14,8 @@ namespace osu.Game.Tournament.Models
[Serializable] [Serializable]
public class TournamentRound public class TournamentRound
{ {
public readonly Bindable<string> Name = new Bindable<string>(); public readonly Bindable<string> Name = new Bindable<string>(string.Empty);
public readonly Bindable<string> Description = new Bindable<string>(); public readonly Bindable<string> Description = new Bindable<string>(string.Empty);
public readonly BindableInt BestOf = new BindableInt(9) { Default = 9, MinValue = 3, MaxValue = 23 }; public readonly BindableInt BestOf = new BindableInt(9) { Default = 9, MinValue = 3, MaxValue = 23 };

View File

@ -149,7 +149,7 @@ namespace osu.Game.Tournament.Screens.Editors
private readonly Bindable<int?> beatmapId = new Bindable<int?>(); private readonly Bindable<int?> beatmapId = new Bindable<int?>();
private readonly Bindable<string> mods = new Bindable<string>(); private readonly Bindable<string> mods = new Bindable<string>(string.Empty);
private readonly Container drawableContainer; private readonly Container drawableContainer;

View File

@ -149,7 +149,7 @@ namespace osu.Game.Tournament.Screens.Editors
private readonly Bindable<int?> beatmapId = new Bindable<int?>(); private readonly Bindable<int?> beatmapId = new Bindable<int?>();
private readonly Bindable<string> score = new Bindable<string>(); private readonly Bindable<string> score = new Bindable<string>(string.Empty);
private readonly Container drawableContainer; private readonly Container drawableContainer;

View File

@ -127,7 +127,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth); score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth);
} }
private class MatchScoreCounter : ScoreCounter private class MatchScoreCounter : CommaSeparatedScoreCounter
{ {
private OsuSpriteText displayedSpriteText; private OsuSpriteText displayedSpriteText;

View File

@ -4,7 +4,6 @@
using System.Diagnostics; using System.Diagnostics;
using ManagedBass.Fx; using ManagedBass.Fx;
using osu.Framework.Audio.Mixing; using osu.Framework.Audio.Mixing;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
namespace osu.Game.Audio.Effects namespace osu.Game.Audio.Effects
@ -21,10 +20,25 @@ namespace osu.Game.Audio.Effects
private readonly BQFParameters filter; private readonly BQFParameters filter;
private readonly BQFType type; private readonly BQFType type;
private bool isAttached;
private int cutoff;
/// <summary> /// <summary>
/// The current cutoff of this filter. /// The cutoff frequency of this filter.
/// </summary> /// </summary>
public BindableNumber<int> Cutoff { get; } public int Cutoff
{
get => cutoff;
set
{
if (value == cutoff)
return;
cutoff = value;
updateFilter(cutoff);
}
}
/// <summary> /// <summary>
/// A Component that implements a BASS FX BiQuad Filter Effect. /// A Component that implements a BASS FX BiQuad Filter Effect.
@ -36,102 +50,96 @@ namespace osu.Game.Audio.Effects
this.mixer = mixer; this.mixer = mixer;
this.type = type; 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<int>(initialCutoff)
{
MinValue = 1,
MaxValue = MAX_LOWPASS_CUTOFF
};
filter = new BQFParameters filter = new BQFParameters
{ {
lFilter = type, lFilter = type,
fCenter = initialCutoff,
fBandwidth = 0, 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) Cutoff = getInitialCutoff(type);
if (type != BQFType.LowPass && type != BQFType.HighPass)
attachFilter();
Cutoff.ValueChanged += updateFilter;
} }
private void attachFilter() private int getInitialCutoff(BQFType type)
{ {
Debug.Assert(!mixer.Effects.Contains(filter)); switch (type)
mixer.Effects.Add(filter);
}
private void detachFilter()
{
Debug.Assert(mixer.Effects.Contains(filter));
mixer.Effects.Remove(filter);
}
private void updateFilter(ValueChangedEvent<int> cutoff)
{
// Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
if (type == BQFType.LowPass)
{ {
if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF) case BQFType.HighPass:
{ return 1;
detachFilter();
return;
}
if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF) case BQFType.LowPass:
attachFilter(); 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. ensureAttached();
if (type == BQFType.HighPass)
{
if (cutoff.NewValue <= 1)
{
detachFilter();
return;
}
if (cutoff.OldValue <= 1 && cutoff.NewValue > 1)
attachFilter();
}
var filterIndex = mixer.Effects.IndexOf(filter); var filterIndex = mixer.Effects.IndexOf(filter);
if (filterIndex < 0) return; if (filterIndex < 0) return;
if (mixer.Effects[filterIndex] is BQFParameters existingFilter) if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
{ {
existingFilter.fCenter = cutoff.NewValue; existingFilter.fCenter = newValue;
// required to update effect with new parameters. // required to update effect with new parameters.
mixer.Effects[filterIndex] = existingFilter; 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) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
ensureDetached();
if (mixer.Effects.Contains(filter))
detachFilter();
} }
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms; using osu.Framework.Graphics.Transforms;
@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects
/// <summary> /// <summary>
/// The filter cutoff. /// The filter cutoff.
/// </summary> /// </summary>
BindableNumber<int> Cutoff { get; } int Cutoff { get; set; }
} }
public static class FilterableAudioComponentExtensions public static class FilterableAudioComponentExtensions
@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects
public static TransformSequence<T> CutoffTo<T, TEasing>(this T component, int newCutoff, double duration, TEasing easing) public static TransformSequence<T> CutoffTo<T, TEasing>(this T component, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction where TEasing : IEasingFunction
=> component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing); => component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing);
/// <summary> /// <summary>
/// Smoothly adjusts filter cutoff over time. /// Smoothly adjusts filter cutoff over time.
@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects
public static TransformSequence<T> CutoffTo<T, TEasing>(this TransformSequence<T> sequence, int newCutoff, double duration, TEasing easing) public static TransformSequence<T> CutoffTo<T, TEasing>(this TransformSequence<T> sequence, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction where TEasing : IEasingFunction
=> sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing)); => sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing));
} }
} }

View File

@ -40,7 +40,13 @@ namespace osu.Game.Beatmaps
public IBeatmap Convert(CancellationToken cancellationToken = default) public IBeatmap Convert(CancellationToken cancellationToken = default)
{ {
// We always operate on a clone of the original beatmap, to not modify it game-wide // 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);
} }
/// <summary> /// <summary>

View File

@ -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); return new BeatmapModelDownloader(modelManager, api, host);
} }

View File

@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
new DownloadBeatmapSetRequest(set, 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) : base(beatmapModelManager, api, host)
{ {
} }

View File

@ -10,7 +10,7 @@ namespace osu.Game.Configuration
[Description("Never repeat")] [Description("Never repeat")]
RandomPermutation, RandomPermutation,
[Description("Random")] [Description("True Random")]
Random Random
} }
} }

View File

@ -116,7 +116,7 @@ namespace osu.Game.Database
/// <param name="paths">One or more archive locations on disk.</param> /// <param name="paths">One or more archive locations on disk.</param>
public Task Import(params string[] paths) public Task Import(params string[] paths)
{ {
var notification = new ProgressNotification { State = ProgressNotificationState.Active }; var notification = new ImportProgressNotification();
PostNotification?.Invoke(notification); PostNotification?.Invoke(notification);
@ -125,7 +125,7 @@ namespace osu.Game.Database
public Task Import(params ImportTask[] tasks) public Task Import(params ImportTask[] tasks)
{ {
var notification = new ProgressNotification { State = ProgressNotificationState.Active }; var notification = new ImportProgressNotification();
PostNotification?.Invoke(notification); PostNotification?.Invoke(notification);

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Models;
#nullable enable
namespace osu.Game.Database
{
/// <summary>
/// A model that contains a list of files it is responsible for.
/// </summary>
public interface IHasRealmFiles
{
IList<RealmNamedFileUsage> Files { get; }
string Hash { get; set; }
}
}

View File

@ -10,7 +10,7 @@ using osu.Game.Overlays.Notifications;
namespace osu.Game.Database namespace osu.Game.Database
{ {
/// <summary> /// <summary>
/// A class which handles importing of asociated models to the game store. /// A class which handles importing of associated models to the game store.
/// </summary> /// </summary>
/// <typeparam name="TModel">The model type.</typeparam> /// <typeparam name="TModel">The model type.</typeparam>
public interface IModelImporter<TModel> : IPostNotifications public interface IModelImporter<TModel> : IPostNotifications

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Models;
#nullable enable
namespace osu.Game.Database
{
/// <summary>
/// Represents a join model which gives a filename and scope to a <see cref="File"/>.
/// </summary>
public interface INamedFile
{
string Filename { get; set; }
RealmFile File { get; set; }
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Overlays.Notifications;
namespace osu.Game.Database
{
public class ImportProgressNotification : ProgressNotification
{
public ImportProgressNotification()
{
State = ProgressNotificationState.Active;
}
}
}

View File

@ -135,9 +135,8 @@ namespace osu.Game.Database
if (IsDisposed) if (IsDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory)); throw new ObjectDisposedException(nameof(RealmContextFactory));
// TODO: this can be added for safety once we figure how to bypass in test if (!ThreadSafety.IsUpdateThread)
// if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread.");
// throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread.");
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);

View File

@ -0,0 +1,111 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using Realms;
#nullable enable
namespace osu.Game.Database
{
/// <summary>
/// Provides a method of working with realm objects over longer application lifetimes.
/// </summary>
/// <typeparam name="T">The underlying object type.</typeparam>
public class RealmLive<T> : ILive<T> where T : RealmObject, IHasGuidPrimaryKey
{
public Guid ID { get; }
private readonly SynchronizationContext? fetchedContext;
private readonly int fetchedThreadId;
/// <summary>
/// The original live data used to create this instance.
/// </summary>
private readonly T data;
/// <summary>
/// Construct a new instance of live realm data.
/// </summary>
/// <param name="data">The realm data.</param>
public RealmLive(T data)
{
this.data = data;
fetchedContext = SynchronizationContext.Current;
fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
ID = data.ID;
}
/// <summary>
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public void PerformRead(Action<T> perform)
{
if (originalDataValid)
{
perform(data);
return;
}
using (var realm = Realm.GetInstance(data.Realm.Config))
perform(realm.Find<T>(ID));
}
/// <summary>
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public TReturn PerformRead<TReturn>(Func<T, TReturn> 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<T>(ID));
}
/// <summary>
/// Perform a write operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public void PerformWrite(Action<T> 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<T>(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;
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using AutoMapper; using AutoMapper;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using Realms; using Realms;
@ -47,5 +48,17 @@ namespace osu.Game.Database
return mapper.Map<T>(item); return mapper.Map<T>(item);
} }
public static List<RealmLive<T>> ToLive<T>(this IEnumerable<T> realmList)
where T : RealmObject, IHasGuidPrimaryKey
{
return realmList.Select(l => new RealmLive<T>(l)).ToList();
}
public static RealmLive<T> ToLive<T>(this T realmObject)
where T : RealmObject, IHasGuidPrimaryKey
{
return new RealmLive<T>(realmObject);
}
} }
} }

View File

@ -225,6 +225,16 @@ namespace osu.Game.Graphics
public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee"); public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee");
public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff"); public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff");
/// <summary>
/// Equivalent to <see cref="OverlayColourProvider.Pink"/>'s <see cref="OverlayColourProvider.Colour3"/>.
/// </summary>
public readonly Color4 Pink3 = Color4Extensions.FromHex(@"cc3378");
/// <summary>
/// Equivalent to <see cref="OverlayColourProvider.Blue"/>'s <see cref="OverlayColourProvider.Colour3"/>.
/// </summary>
public readonly Color4 Blue3 = Color4Extensions.FromHex(@"3399cc");
/// <summary> /// <summary>
/// Equivalent to <see cref="OverlayColourProvider.Lime"/>'s <see cref="OverlayColourProvider.Colour1"/>. /// Equivalent to <see cref="OverlayColourProvider.Lime"/>'s <see cref="OverlayColourProvider.Colour1"/>.
/// </summary> /// </summary>

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.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<double>
{
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));
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -13,43 +14,30 @@ namespace osu.Game.Graphics.UserInterface
protected override double RollingDuration => 1000; protected override double RollingDuration => 1000;
protected override Easing RollingEasing => Easing.Out; protected override Easing RollingEasing => Easing.Out;
/// <summary>
/// Whether comma separators should be displayed.
/// </summary>
public bool UseCommaSeparator { get; }
public Bindable<int> RequiredDisplayDigits { get; } = new Bindable<int>(); public Bindable<int> RequiredDisplayDigits { get; } = new Bindable<int>();
private string formatString;
/// <summary> /// <summary>
/// Displays score. /// Displays score.
/// </summary> /// </summary>
/// <param name="leading">How many leading zeroes the counter will have.</param> /// <param name="leading">How many leading zeroes the counter will have.</param>
/// <param name="useCommaSeparator">Whether comma separators should be displayed.</param> protected ScoreCounter(int leading = 0)
protected ScoreCounter(int leading = 0, bool useCommaSeparator = false)
{ {
UseCommaSeparator = useCommaSeparator;
RequiredDisplayDigits.Value = leading; RequiredDisplayDigits.Value = leading;
RequiredDisplayDigits.BindValueChanged(_ => UpdateDisplay()); RequiredDisplayDigits.BindValueChanged(displayDigitsChanged, true);
} }
protected override double GetProportionalDuration(double currentValue, double newValue) private void displayDigitsChanged(ValueChangedEvent<int> _)
{ {
return currentValue > newValue ? currentValue - newValue : newValue - currentValue; formatString = new string('0', RequiredDisplayDigits.Value);
UpdateDisplay();
} }
protected override LocalisableString FormatCount(double count) protected override double GetProportionalDuration(double currentValue, double newValue) =>
{ currentValue > newValue ? currentValue - newValue : newValue - currentValue;
string format = new string('0', RequiredDisplayDigits.Value);
if (UseCommaSeparator) protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(formatString);
{
for (int i = format.Length - 3; i > 0; i -= 3)
format = format.Insert(i, @",");
}
return ((long)count).ToString(format);
}
protected override OsuSpriteText CreateSpriteText() protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true));

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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<string> FilterTerms => new[] { Text.ToString() };
public bool MatchingFilter
{
set => this.FadeTo(value ? 1 : 0);
}
public bool FilteringActive { get; set; }
}
}

View File

@ -24,6 +24,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString VolumeHeader => new TranslatableString(getKey(@"volume_header"), @"Volume"); public static LocalisableString VolumeHeader => new TranslatableString(getKey(@"volume_header"), @"Volume");
/// <summary>
/// "Output device"
/// </summary>
public static LocalisableString OutputDevice => new TranslatableString(getKey(@"output_device"), @"Output device");
/// <summary> /// <summary>
/// "Master" /// "Master"
/// </summary> /// </summary>

View File

@ -14,11 +14,36 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString GameplaySectionHeader => new TranslatableString(getKey(@"gameplay_section_header"), @"Gameplay"); public static LocalisableString GameplaySectionHeader => new TranslatableString(getKey(@"gameplay_section_header"), @"Gameplay");
/// <summary>
/// "Beatmap"
/// </summary>
public static LocalisableString BeatmapHeader => new TranslatableString(getKey(@"beatmap_header"), @"Beatmap");
/// <summary> /// <summary>
/// "General" /// "General"
/// </summary> /// </summary>
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General"); public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
/// <summary>
/// "Audio"
/// </summary>
public static LocalisableString AudioHeader => new TranslatableString(getKey(@"audio"), @"Audio");
/// <summary>
/// "HUD"
/// </summary>
public static LocalisableString HUDHeader => new TranslatableString(getKey(@"h_u_d"), @"HUD");
/// <summary>
/// "Input"
/// </summary>
public static LocalisableString InputHeader => new TranslatableString(getKey(@"input"), @"Input");
/// <summary>
/// "Background"
/// </summary>
public static LocalisableString BackgroundHeader => new TranslatableString(getKey(@"background"), @"Background");
/// <summary> /// <summary>
/// "Background dim" /// "Background dim"
/// </summary> /// </summary>

View File

@ -104,6 +104,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString HitLighting => new TranslatableString(getKey(@"hit_lighting"), @"Hit lighting"); public static LocalisableString HitLighting => new TranslatableString(getKey(@"hit_lighting"), @"Hit lighting");
/// <summary>
/// "Screenshots"
/// </summary>
public static LocalisableString Screenshots => new TranslatableString(getKey(@"screenshots"), @"Screenshots");
/// <summary> /// <summary>
/// "Screenshot format" /// "Screenshot format"
/// </summary> /// </summary>

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class RulesetSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.RulesetSettings";
/// <summary>
/// "Rulesets"
/// </summary>
public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -14,6 +14,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString SkinSectionHeader => new TranslatableString(getKey(@"skin_section_header"), @"Skin"); public static LocalisableString SkinSectionHeader => new TranslatableString(getKey(@"skin_section_header"), @"Skin");
/// <summary>
/// "Current skin"
/// </summary>
public static LocalisableString CurrentSkin => new TranslatableString(getKey(@"current_skin"), @"Current skin");
/// <summary> /// <summary>
/// "Skin layout editor" /// "Skin layout editor"
/// </summary> /// </summary>

View File

@ -0,0 +1,117 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using 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
{
/// <summary>
/// A single beatmap difficulty.
/// </summary>
[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;
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.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;
/// <summary>
/// Returns a shallow-clone of this <see cref="RealmBeatmapDifficulty"/>.
/// </summary>
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;
}
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using 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;
/// <summary>
/// The time in milliseconds to begin playing the track for preview purposes.
/// If -1, the track should begin playing at 40% of its length.
/// </summary>
public int PreviewTime { get; set; }
public string AudioFile { get; set; } = string.Empty;
public string BackgroundFile { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,78 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.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<RealmBeatmapSet>, 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<RealmBeatmap> Beatmaps { get; } = null!;
public IList<RealmNamedFileUsage> Files { get; } = null!;
public bool DeletePending { get; set; }
public string Hash { get; set; } = string.Empty;
/// <summary>
/// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present).
/// </summary>
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);
/// <summary>
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
/// </summary>
/// <param name="filename">The name of the file to get the storage path of.</param>
public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.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<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps;
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => Files;
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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);
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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;
}
}

View File

@ -0,0 +1,63 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using 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<RealmRuleset>, 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
};
}
}

View File

@ -177,6 +177,24 @@ namespace osu.Game.Online.Chat
case "wiki": case "wiki":
return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3))); 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, SearchBeatmapSet,
OpenWiki, OpenWiki,
Custom, Custom,
OpenChangelog,
} }
public class Link : IComparable<Link> public class Link : IComparable<Link>

View File

@ -90,6 +90,8 @@ namespace osu.Game
private WikiOverlay wikiOverlay; private WikiOverlay wikiOverlay;
private ChangelogOverlay changelogOverlay;
private SkinEditorOverlay skinEditor; private SkinEditorOverlay skinEditor;
private Container overlayContent; private Container overlayContent;
@ -209,13 +211,6 @@ namespace osu.Game
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() 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.CacheAs(this);
dependencies.Cache(SentryLogger); dependencies.Cache(SentryLogger);
@ -336,6 +331,17 @@ namespace osu.Game
ShowWiki(link.Argument); ShowWiki(link.Argument);
break; break;
case LinkAction.OpenChangelog:
if (string.IsNullOrEmpty(link.Argument))
ShowChangelogListing();
else
{
var changelogArgs = link.Argument.Split("/");
ShowChangelogBuild(changelogArgs[0], changelogArgs[1]);
}
break;
default: default:
throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action."); throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
} }
@ -401,6 +407,18 @@ namespace osu.Game
/// <param name="path">The wiki page to show</param> /// <param name="path">The wiki page to show</param>
public void ShowWiki(string path) => waitForReady(() => wikiOverlay, _ => wikiOverlay.ShowPage(path)); public void ShowWiki(string path) => waitForReady(() => wikiOverlay, _ => wikiOverlay.ShowPage(path));
/// <summary>
/// Show changelog listing overlay
/// </summary>
public void ShowChangelogListing() => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowListing());
/// <summary>
/// Show changelog's build as an overlay
/// </summary>
/// <param name="updateStream">The update stream name</param>
/// <param name="version">The build version of the update stream</param>
public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
/// <summary> /// <summary>
/// Present a beatmap at song select immediately. /// Present a beatmap at song select immediately.
/// The user should have already requested this interactively. /// The user should have already requested this interactively.
@ -769,7 +787,7 @@ namespace osu.Game
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
loadComponentSingleFile(new MessageNotifier(), AddInternal, true); loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, 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(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true); loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true);
@ -842,6 +860,19 @@ namespace osu.Game
{ {
if (mode.NewValue != OverlayActivation.All) CloseAllOverlays(); 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) private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -410,11 +411,28 @@ namespace osu.Game
{ {
Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""..."); 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)); (Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
} }
finally
{
realmBlocker?.Dispose();
}
Logger.Log(@"Migration complete!"); Logger.Log(@"Migration complete!");
} }

View File

@ -29,7 +29,7 @@ namespace osu.Game.Overlays.OSD
private Sample sampleChange; private Sample sampleChange;
public TrackedSettingToast(SettingDescription description) public TrackedSettingToast(SettingDescription description)
: base(description.Name, description.Value, description.Shortcut) : base(description.Name.ToString(), description.Value.ToString(), description.Shortcut.ToString())
{ {
FillFlowContainer<OptionLight> optionLights; FillFlowContainer<OptionLight> optionLights;

View File

@ -14,10 +14,7 @@ namespace osu.Game.Overlays.Settings
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
BackgroundColour = colours.Pink; BackgroundColour = colours.Pink3;
Triangles.ColourDark = colours.PinkDark;
Triangles.ColourLight = colours.PinkLight;
} }
} }
} }

View File

@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{ {
dropdown = new AudioDeviceSettingsDropdown dropdown = new AudioDeviceSettingsDropdown
{ {
LabelText = AudioSettingsStrings.OutputDevice,
Keywords = new[] { "speaker", "headphone", "output" } Keywords = new[] { "speaker", "headphone", "output" }
} }
}; };

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.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<bool>(OsuSetting.PositionalHitSounds)
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak,
Current = config.GetBindable<bool>(OsuSetting.AlwaysPlayFirstComboBreak)
},
};
}
}
}

View File

@ -0,0 +1,48 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.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<double>
{
LabelText = GameplaySettingsStrings.BackgroundDim,
Current = config.GetBindable<double>(OsuSetting.DimLevel),
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
new SettingsSlider<double>
{
LabelText = GameplaySettingsStrings.BackgroundBlur,
Current = config.GetBindable<double>(OsuSetting.BlurLevel),
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.LightenDuringBreaks,
Current = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks)
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.FadePlayfieldWhenHealthLow,
Current = config.GetBindable<bool>(OsuSetting.FadePlayfieldWhenHealthLow),
},
};
}
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.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<bool>(OsuSetting.BeatmapSkins)
},
new SettingsCheckbox
{
LabelText = SkinSettingsStrings.BeatmapColours,
Current = config.GetBindable<bool>(OsuSetting.BeatmapColours)
},
new SettingsCheckbox
{
LabelText = SkinSettingsStrings.BeatmapHitsounds,
Current = config.GetBindable<bool>(OsuSetting.BeatmapHitsounds)
},
new SettingsCheckbox
{
LabelText = GraphicsSettingsStrings.StoryboardVideo,
Current = config.GetBindable<bool>(OsuSetting.ShowStoryboard)
},
};
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -20,77 +19,18 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new SettingsSlider<double>
{
LabelText = GameplaySettingsStrings.BackgroundDim,
Current = config.GetBindable<double>(OsuSetting.DimLevel),
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
new SettingsSlider<double>
{
LabelText = GameplaySettingsStrings.BackgroundBlur,
Current = config.GetBindable<double>(OsuSetting.BlurLevel),
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.LightenDuringBreaks,
Current = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks)
},
new SettingsEnumDropdown<HUDVisibilityMode>
{
LabelText = GameplaySettingsStrings.HUDVisibilityMode,
Current = config.GetBindable<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode)
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.ShowDifficultyGraph,
Current = config.GetBindable<bool>(OsuSetting.ShowProgressGraph)
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail,
Current = config.GetBindable<bool>(OsuSetting.ShowHealthDisplayWhenCantFail),
Keywords = new[] { "hp", "bar" }
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.FadePlayfieldWhenHealthLow,
Current = config.GetBindable<bool>(OsuSetting.FadePlayfieldWhenHealthLow),
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay,
Current = config.GetBindable<bool>(OsuSetting.KeyOverlay)
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.PositionalHitsounds,
Current = config.GetBindable<bool>(OsuSetting.PositionalHitSounds)
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak,
Current = config.GetBindable<bool>(OsuSetting.AlwaysPlayFirstComboBreak)
},
new SettingsEnumDropdown<ScoringMode> new SettingsEnumDropdown<ScoringMode>
{ {
LabelText = GameplaySettingsStrings.ScoreDisplayMode, LabelText = GameplaySettingsStrings.ScoreDisplayMode,
Current = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode), Current = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode),
Keywords = new[] { "scoring" } Keywords = new[] { "scoring" }
}, },
}; new SettingsCheckbox
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
Add(new SettingsCheckbox
{ {
LabelText = GameplaySettingsStrings.DisableWinKey, LabelText = GraphicsSettingsStrings.HitLighting,
Current = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey) Current = config.GetBindable<bool>(OsuSetting.HitLighting)
}); },
} };
} }
} }
} }

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.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<HUDVisibilityMode>
{
LabelText = GameplaySettingsStrings.HUDVisibilityMode,
Current = config.GetBindable<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode)
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.ShowDifficultyGraph,
Current = config.GetBindable<bool>(OsuSetting.ShowProgressGraph)
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail,
Current = config.GetBindable<bool>(OsuSetting.ShowHealthDisplayWhenCantFail),
Keywords = new[] { "hp", "bar" }
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay,
Current = config.GetBindable<bool>(OsuSetting.KeyOverlay)
},
};
}
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework;
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<float, SizeSlider>
{
LabelText = SkinSettingsStrings.GameplayCursorSize,
Current = config.GetBindable<float>(OsuSetting.GameplayCursorSize),
KeyboardStep = 0.01f
},
new SettingsCheckbox
{
LabelText = SkinSettingsStrings.AutoCursorSize,
Current = config.GetBindable<bool>(OsuSetting.AutoCursorSize)
},
};
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
Add(new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.DisableWinKey,
Current = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey)
});
}
}
}
}

View File

@ -1,16 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics; 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.Graphics.Sprites;
using osu.Framework.Logging;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Overlays.Settings.Sections.Gameplay;
namespace osu.Game.Overlays.Settings.Sections namespace osu.Game.Overlays.Settings.Sections
{ {
@ -20,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections
public override Drawable CreateIcon() => new SpriteIcon public override Drawable CreateIcon() => new SpriteIcon
{ {
Icon = FontAwesome.Regular.Circle Icon = FontAwesome.Regular.DotCircle
}; };
public GameplaySection() public GameplaySection()
@ -28,27 +23,13 @@ namespace osu.Game.Overlays.Settings.Sections
Children = new Drawable[] Children = new Drawable[]
{ {
new GeneralSettings(), new GeneralSettings(),
new AudioSettings(),
new BeatmapSettings(),
new BackgroundSettings(),
new HUDSettings(),
new InputSettings(),
new ModsSettings(), 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");
}
}
}
} }
} }

View File

@ -9,25 +9,15 @@ using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Graphics 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] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new SettingsCheckbox
{
LabelText = GraphicsSettingsStrings.StoryboardVideo,
Current = config.GetBindable<bool>(OsuSetting.ShowStoryboard)
},
new SettingsCheckbox
{
LabelText = GraphicsSettingsStrings.HitLighting,
Current = config.GetBindable<bool>(OsuSetting.HitLighting)
},
new SettingsEnumDropdown<ScreenshotFormat> new SettingsEnumDropdown<ScreenshotFormat>
{ {
LabelText = GraphicsSettingsStrings.ScreenshotFormat, LabelText = GraphicsSettingsStrings.ScreenshotFormat,

View File

@ -22,9 +22,9 @@ namespace osu.Game.Overlays.Settings.Sections
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new RendererSettings(),
new LayoutSettings(), new LayoutSettings(),
new DetailSettings(), new RendererSettings(),
new ScreenshotSettings(),
}; };
} }
} }

View File

@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private RealmContextFactory realmFactory { get; set; } private RealmContextFactory realmFactory { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OverlayColourProvider colourProvider)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -101,7 +101,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Radius = 2, Radius = 2,
Colour = colours.YellowDark.Opacity(0), Colour = colourProvider.Highlight1.Opacity(0),
Type = EdgeEffectType.Shadow, Type = EdgeEffectType.Shadow,
Hollow = true, Hollow = true,
}, },
@ -110,13 +110,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
new Box new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black, Colour = colourProvider.Background5,
Alpha = 0.6f,
}, },
text = new OsuSpriteText text = new OsuSpriteText
{ {
Text = action.GetLocalisableDescription(), Text = action.GetLocalisableDescription(),
Margin = new MarginPadding(padding), Margin = new MarginPadding(1.5f * padding),
}, },
buttons = new FillFlowContainer<KeyButton> buttons = new FillFlowContainer<KeyButton>
{ {
@ -405,7 +404,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private readonly Box box; private readonly Box box;
public readonly OsuSpriteText Text; public readonly OsuSpriteText Text;
private Color4 hoverColour; [Resolved]
private OverlayColourProvider colourProvider { get; set; }
private bool isBinding; private bool isBinding;
@ -448,7 +448,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
box = new Box box = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black
}, },
Text = new OsuSpriteText Text = new OsuSpriteText
{ {
@ -463,9 +462,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load()
{ {
hoverColour = colours.YellowDark; updateHoverState();
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
@ -484,12 +483,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{ {
if (isBinding) 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); Text.FadeColour(Color4.Black, transition_time, Easing.OutQuint);
} }
else 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); Text.FadeColour(IsHovered ? Color4.Black : Color4.White, transition_time, Easing.OutQuint);
} }
} }

View File

@ -7,7 +7,6 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Localisation; using osu.Game.Localisation;
@ -27,8 +26,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{ {
this.variant = variant; this.variant = variant;
FlowContent.Spacing = new Vector2(0, 1); FlowContent.Spacing = new Vector2(0, 3);
FlowContent.Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS };
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -60,7 +58,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
} }
} }
public class ResetButton : DangerousTriangleButton public class ResetButton : DangerousSettingsButton
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -8,16 +9,24 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Input.Handlers.Tablet;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Overlays.Settings.Sections.Input 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 readonly ITabletHandler tabletHandler;
private Bindable<float> rotation; private Bindable<float> rotation;
private readonly RotationButton[] rotationPresets = new RotationButton[preset_count];
private const int preset_count = 4;
private const int height = 50; private const int height = 50;
public RotationPresetButtons(ITabletHandler tabletHandler) public RotationPresetButtons(ITabletHandler tabletHandler)
@ -27,18 +36,39 @@ namespace osu.Game.Overlays.Settings.Sections.Input
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = height; Height = height;
for (int i = 0; i < 360; i += 90) IEnumerable<Dimension> createColumns(int count)
{ {
var presetRotation = i; for (int i = 0; i < count; ++i)
Add(new RotationButton(i)
{ {
RelativeSizeAxes = Axes.X, if (i > 0)
Height = height, yield return new Dimension(GridSizeMode.Absolute, 10);
Width = 0.25f,
Text = $@"{presetRotation}º", yield return new Dimension();
Action = () => tabletHandler.Rotation.Value = presetRotation, }
}); }
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 = tabletHandler.Rotation.GetBoundCopy();
rotation.BindValueChanged(val => rotation.BindValueChanged(val =>
{ {
foreach (var b in Children.OfType<RotationButton>()) foreach (var b in rotationPresets)
b.IsSelected = b.Preset == val.NewValue; b.IsSelected = b.Preset == val.NewValue;
}, true); }, true);
} }
public class RotationButton : TriangleButton public class RotationButton : RoundedButton
{ {
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
public readonly int Preset; public readonly int Preset;
public RotationButton(int preset) public RotationButton(int preset)
@ -91,18 +124,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private void updateColour() private void updateColour()
{ {
if (isSelected) BackgroundColour = isSelected ? colours.Blue3 : colourProvider.Background3;
{
BackgroundColour = colours.BlueDark;
Triangles.ColourDark = colours.BlueDarker;
Triangles.ColourLight = colours.Blue;
}
else
{
BackgroundColour = colours.Gray4;
Triangles.ColourDark = colours.Gray5;
Triangles.ColourLight = colours.Gray6;
}
} }
} }
} }

View File

@ -165,7 +165,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
LabelText = TabletSettingsStrings.Rotation, LabelText = TabletSettingsStrings.Rotation,
Current = rotation Current = rotation
}, },
new RotationPresetButtons(tabletHandler), new RotationPresetButtons(tabletHandler)
{
Padding = new MarginPadding
{
Horizontal = SettingsPanel.CONTENT_MARGINS
}
},
new SettingsSlider<float> new SettingsSlider<float>
{ {
TransferValueOnCommit = true, TransferValueOnCommit = true,

View File

@ -10,7 +10,6 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -21,15 +20,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
protected override LocalisableString Header => "General"; protected override LocalisableString Header => "General";
private TriangleButton importBeatmapsButton; private SettingsButton importBeatmapsButton;
private TriangleButton importScoresButton; private SettingsButton importScoresButton;
private TriangleButton importSkinsButton; private SettingsButton importSkinsButton;
private TriangleButton importCollectionsButton; private SettingsButton importCollectionsButton;
private TriangleButton deleteBeatmapsButton; private SettingsButton deleteBeatmapsButton;
private TriangleButton deleteScoresButton; private SettingsButton deleteScoresButton;
private TriangleButton deleteSkinsButton; private SettingsButton deleteSkinsButton;
private TriangleButton restoreButton; private SettingsButton restoreButton;
private TriangleButton undeleteButton; private SettingsButton undeleteButton;
[BackgroundDependencyLoader(permitNulls: true)] [BackgroundDependencyLoader(permitNulls: true)]
private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] StableImportManager stableImportManager, DialogOverlay dialogOverlay) private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] StableImportManager stableImportManager, DialogOverlay dialogOverlay)

View File

@ -6,7 +6,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Overlays.Settings.Sections.Maintenance; using osu.Game.Overlays.Settings.Sections.Maintenance;
using osuTK;
namespace osu.Game.Overlays.Settings.Sections namespace osu.Game.Overlays.Settings.Sections
{ {
@ -21,7 +20,6 @@ namespace osu.Game.Overlays.Settings.Sections
public MaintenanceSection() public MaintenanceSection()
{ {
FlowContent.Spacing = new Vector2(0, 5);
Children = new Drawable[] Children = new Drawable[]
{ {
new GeneralSettings() new GeneralSettings()

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.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");
}
}
}
}
}

View File

@ -16,7 +16,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Skinning.Editor; using osu.Game.Skinning.Editor;
using osuTK;
namespace osu.Game.Overlays.Settings.Sections namespace osu.Game.Overlays.Settings.Sections
{ {
@ -63,43 +62,18 @@ namespace osu.Game.Overlays.Settings.Sections
[BackgroundDependencyLoader(permitNulls: true)] [BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor) private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor)
{ {
FlowContent.Spacing = new Vector2(0, 5);
Children = new Drawable[] Children = new Drawable[]
{ {
skinDropdown = new SkinSettingsDropdown(), skinDropdown = new SkinSettingsDropdown
{
LabelText = SkinSettingsStrings.CurrentSkin
},
new SettingsButton new SettingsButton
{ {
Text = SkinSettingsStrings.SkinLayoutEditor, Text = SkinSettingsStrings.SkinLayoutEditor,
Action = () => skinEditor?.Toggle(), Action = () => skinEditor?.Toggle(),
}, },
new ExportSkinButton(), new ExportSkinButton(),
new SettingsSlider<float, SizeSlider>
{
LabelText = SkinSettingsStrings.GameplayCursorSize,
Current = config.GetBindable<float>(OsuSetting.GameplayCursorSize),
KeyboardStep = 0.01f
},
new SettingsCheckbox
{
LabelText = SkinSettingsStrings.AutoCursorSize,
Current = config.GetBindable<bool>(OsuSetting.AutoCursorSize)
},
new SettingsCheckbox
{
LabelText = SkinSettingsStrings.BeatmapSkins,
Current = config.GetBindable<bool>(OsuSetting.BeatmapSkins)
},
new SettingsCheckbox
{
LabelText = SkinSettingsStrings.BeatmapColours,
Current = config.GetBindable<bool>(OsuSetting.BeatmapColours)
},
new SettingsCheckbox
{
LabelText = SkinSettingsStrings.BeatmapHitsounds,
Current = config.GetBindable<bool>(OsuSetting.BeatmapHitsounds)
},
}; };
managerUpdated = skins.ItemUpdated.GetBoundCopy(); managerUpdated = skins.ItemUpdated.GetBoundCopy();

View File

@ -6,11 +6,11 @@ using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
public class SettingsButton : TriangleButton, IHasTooltip public class SettingsButton : RoundedButton, IHasTooltip
{ {
public SettingsButton() public SettingsButton()
{ {

View File

@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Settings
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OverlayColourProvider colourProvider)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Settings
new OsuSpriteText new OsuSpriteText
{ {
Text = heading, Text = heading,
Font = OsuFont.GetFont(size: 40), Font = OsuFont.TorusAlternate.With(size: 40),
Margin = new MarginPadding Margin = new MarginPadding
{ {
Left = SettingsPanel.CONTENT_MARGINS, Left = SettingsPanel.CONTENT_MARGINS,
@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Settings
}, },
new OsuSpriteText new OsuSpriteText
{ {
Colour = colours.Pink, Colour = colourProvider.Content2,
Text = subheading, Text = subheading,
Font = OsuFont.GetFont(size: 18), Font = OsuFont.GetFont(size: 18),
Margin = new MarginPadding Margin = new MarginPadding

View File

@ -12,7 +12,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK.Graphics; using osuTK;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
@ -31,9 +31,10 @@ namespace osu.Game.Overlays.Settings
public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>(); public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>();
public virtual IEnumerable<string> FilterTerms => new[] { Header.ToString() }; public virtual IEnumerable<string> FilterTerms => new[] { Header.ToString() };
private const int header_size = 26; public const int ITEM_SPACING = 14;
private const int margin = 20;
private const int border_size = 2; private const int header_size = 24;
private const int border_size = 4;
public bool MatchingFilter public bool MatchingFilter
{ {
@ -54,8 +55,9 @@ namespace osu.Game.Overlays.Settings
{ {
Margin = new MarginPadding Margin = new MarginPadding
{ {
Top = header_size Top = 36
}, },
Spacing = new Vector2(0, ITEM_SPACING),
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -63,14 +65,14 @@ namespace osu.Game.Overlays.Settings
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OverlayColourProvider colourProvider, OsuColour colours)
{ {
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
new Box new Box
{ {
Name = "separator", Name = "separator",
Colour = new Color4(0, 0, 0, 255), Colour = colourProvider.Background6,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = border_size, Height = border_size,
}, },
@ -78,8 +80,8 @@ namespace osu.Game.Overlays.Settings
{ {
Padding = new MarginPadding Padding = new MarginPadding
{ {
Top = margin + border_size, Top = 28,
Bottom = margin + 10, Bottom = 40,
}, },
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -87,13 +89,11 @@ namespace osu.Game.Overlays.Settings
{ {
header = new OsuSpriteText header = new OsuSpriteText
{ {
Font = OsuFont.GetFont(size: header_size), Font = OsuFont.TorusAlternate.With(size: header_size),
Text = Header, Text = Header,
Colour = colours.Yellow,
Margin = new MarginPadding Margin = new MarginPadding
{ {
Left = SettingsPanel.CONTENT_MARGINS, Horizontal = SettingsPanel.CONTENT_MARGINS
Right = SettingsPanel.CONTENT_MARGINS
} }
}, },
FlowContent FlowContent

View File

@ -46,13 +46,17 @@ namespace osu.Game.Overlays.Settings
FlowContent = new FillFlowContainer FlowContent = new FillFlowContainer
{ {
Margin = new MarginPadding { Top = SettingsSection.ITEM_SPACING },
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 8), Spacing = new Vector2(0, SettingsSection.ITEM_SPACING),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
}; };
} }
private const int header_height = 43;
private const int header_font_size = 20;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -60,9 +64,9 @@ namespace osu.Game.Overlays.Settings
{ {
new OsuSpriteText new OsuSpriteText
{ {
Text = Header.ToString().ToUpper(), // TODO: Add localisation support after https://github.com/ppy/osu-framework/pull/4603 is merged. Text = Header,
Margin = new MarginPadding { Vertical = 30, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, Margin = new MarginPadding { Vertical = (header_height - header_font_size) * 0.5f, Horizontal = SettingsPanel.CONTENT_MARGINS },
Font = OsuFont.GetFont(weight: FontWeight.Bold), Font = OsuFont.GetFont(size: header_font_size),
}, },
FlowContent FlowContent
}); });

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
@ -13,5 +15,17 @@ namespace osu.Game.Overlays.Settings
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
CommitOnFocusLost = true CommitOnFocusLost = true
}; };
public override Bindable<string> Current
{
get => base.Current;
set
{
if (value.Default == null)
throw new InvalidOperationException($"Bindable settings of type {nameof(Bindable<string>)} should have a non-null default value.");
base.Current = value;
}
}
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -15,22 +16,23 @@ using osuTK;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
public class Sidebar : Container<SidebarButton>, IStateful<ExpandedState> public class Sidebar : Container<SidebarIconButton>, IStateful<ExpandedState>
{ {
private readonly FillFlowContainer<SidebarButton> content; private readonly Box background;
public const float DEFAULT_WIDTH = Toolbar.Toolbar.HEIGHT * 1.4f; private readonly FillFlowContainer<SidebarIconButton> content;
public const float DEFAULT_WIDTH = 70;
public const int EXPANDED_WIDTH = 200; public const int EXPANDED_WIDTH = 200;
public event Action<ExpandedState> StateChanged; public event Action<ExpandedState> StateChanged;
protected override Container<SidebarButton> Content => content; protected override Container<SidebarIconButton> Content => content;
public Sidebar() public Sidebar()
{ {
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Box background = new Box
{ {
Colour = OsuColour.Gray(0.02f), Colour = OsuColour.Gray(0.02f),
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -39,7 +41,7 @@ namespace osu.Game.Overlays.Settings
{ {
Children = new[] Children = new[]
{ {
content = new FillFlowContainer<SidebarButton> content = new FillFlowContainer<SidebarIconButton>
{ {
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Anchor = 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 ScheduledDelegate expandEvent;
private ExpandedState state; private ExpandedState state;
@ -80,8 +88,6 @@ namespace osu.Game.Overlays.Settings
{ {
public SidebarScrollContainer() public SidebarScrollContainer()
{ {
Content.Anchor = Anchor.CentreLeft;
Content.Origin = Anchor.CentreLeft;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
ScrollbarVisible = false; ScrollbarVisible = false;
} }

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