diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 0c6b80e97e..fc61573416 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1,2 @@
+github: ppy
custom: https://osu.ppy.sh/home/support
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 29cbdd2d37..0da1f9636b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -79,9 +79,14 @@ jobs:
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
- dotnet codefilesanity | while read -r line; do
- echo "::warning::$line"
- done
+ exit_code=0
+ while read -r line; do
+ if [[ ! -z "$line" ]]; then
+ echo "::error::$line"
+ exit_code=1
+ fi
+ done <<< $(dotnet codefilesanity)
+ exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
index e0ccd50989..358cbda17a 100644
--- a/.github/workflows/report-nunit.yml
+++ b/.github/workflows/report-nunit.yml
@@ -30,3 +30,5 @@ jobs:
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
path: "*.trx"
reporter: dotnet-trx
+ list-suites: 'failed'
+ list-tests: 'failed'
diff --git a/osu.Android.props b/osu.Android.props
index fefc2f6438..f552aff2f2 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 0d6925a83d..6d5a960f06 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -42,9 +42,8 @@ namespace osu.Game.Rulesets.Catch.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate;
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index 538a51db5f..5ccb191a9b 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
@@ -13,6 +13,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@@ -101,27 +102,27 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
throw new System.NotImplementedException();
}
- public override float GetBeatSnapDistanceAt(double referenceTime)
+ public override float GetBeatSnapDistanceAt(HitObject referenceObject)
{
throw new System.NotImplementedException();
}
- public override float DurationToDistance(double referenceTime, double duration)
+ public override float DurationToDistance(HitObject referenceObject, double duration)
{
throw new System.NotImplementedException();
}
- public override double DistanceToDuration(double referenceTime, float distance)
+ public override double DistanceToDuration(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
- public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
+ public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
- public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
+ public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 471dad87d5..4387bc6b3b 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -388,7 +388,7 @@ namespace osu.Game.Rulesets.Mania.Tests
},
};
- beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+ beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
}
AddStep("load player", () =>
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
index 18891f8c58..89e13acad6 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 380efff69f..1ed045f7e0 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
- DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
+ DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
double beatLength;
#pragma warning disable 618
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
- beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
+ beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
SpanCount = repeatsData?.SpanCount() ?? 1;
StartTime = (int)Math.Round(hitObject.StartTime);
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
index 7c75b45e5f..ca9e5b0b85 100644
--- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
@@ -13,6 +13,7 @@ SliderTickRate:1
[TimingPoints]
0,500,4,1,0,100,1,0
+10000,-150,4,1,0,100,1,0
[HitObjects]
51,192,500,128,0,1500:1:0:0:0:
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index 3b7da8d9ba..28e970f397 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Mania.UI
// For non-mania beatmap, speed changes should only happen through timing points
if (!isForCurrentRuleset)
- p.DifficultyPoint = new DifficultyControlPoint();
+ p.EffectPoint = new EffectControlPoint();
}
BarLines.ForEach(Playfield.Add);
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
index 851be2b2f2..ef43c3a696 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
@@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
@@ -179,15 +180,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
- public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
+ public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length;
- public float DurationToDistance(double referenceTime, double duration) => (float)duration;
+ public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
- public double DistanceToDuration(double referenceTime, float distance) => distance;
+ public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
- public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
+ public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
- public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
+ public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
index 0ba775e5c7..37f1a846ad 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -45,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
new Spinner
{
- Duration = 2000,
- Position = OsuPlayfield.BASE_SIZE / 2
+ Duration = 6000,
+ Position = OsuPlayfield.BASE_SIZE / 2,
}
}
},
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index f09aad8b49..1f01ba601b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
config.SetValue(OsuSetting.AutoCursorSize, true);
gameplayState.Beatmap.Difficulty.CircleSize = val;
- Scheduler.AddOnce(() => loadContent(false));
+ Scheduler.AddOnce(loadContent);
});
AddStep("test cursor container", () => loadContent(false));
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize);
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
- AddStep("load content", () => loadContent());
+ AddStep("load content", loadContent);
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
@@ -98,7 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
}
- private void loadContent(bool automated = true, Func skinProvider = null)
+ private void loadContent() => loadContent(false);
+
+ private void loadContent(bool automated, Func skinProvider = null)
{
SetContents(_ =>
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
index ececfb0586..d31e7a31f5 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
@@ -407,8 +407,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
-
SelectedMods.Value = new[] { new OsuModClassic() };
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
@@ -439,6 +437,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
+ DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
+
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index 81902c25af..03b4254eed 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -13,6 +13,7 @@ using osuTK.Graphics;
using osu.Game.Rulesets.Mods;
using System.Linq;
using NUnit.Framework;
+using osu.Game.Beatmaps.Legacy;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
@@ -328,10 +329,14 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
{
- var cpi = new ControlPointInfo();
- cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
+ var cpi = new LegacyControlPointInfo();
+ cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
- slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty
+ {
+ CircleSize = circleSize,
+ SliderTickRate = 3
+ });
var drawable = CreateDrawableSlider(slider);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index 590d159300..f3392724ec 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -348,6 +348,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
+ DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f },
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
@@ -362,8 +363,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
-
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index 9da583a073..52ab39cfbd 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
{
+ private const double spinner_start_time = 100;
+ private const double spinner_duration = 6000;
+
[Resolved]
private AudioManager audioManager { get; set; }
@@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
- addSeekStep(5000);
+ addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
{
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
@@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
- addSeekStep(2500);
+ addSeekStep(spinner_start_time + 2500);
AddAssert("disc rotation rewound",
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
@@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
- addSeekStep(5000);
+ addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
@@ -140,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinnerNormalBonusRewinding()
{
- addSeekStep(1000);
+ addSeekStep(spinner_start_time + 1000);
AddAssert("player score matching expected bonus score", () =>
{
@@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
}
- private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
- {
- Frames = scoreReplay
- .Frames
- .Cast()
- .Select(replayFrame =>
- {
- var adjustedTime = replayFrame.Time * rate;
- return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
- })
- .Cast()
- .ToList()
- };
-
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
-
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
@@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests
new Spinner
{
Position = new Vector2(256, 192),
- EndTime = 6000,
+ StartTime = spinner_start_time,
+ Duration = spinner_duration
},
}
};
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
index 1b85e0efde..2d43e1b95e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
@@ -369,8 +369,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
-
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
@@ -399,6 +397,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
+ DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
+
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
index a2fc4848af..d82186fb52 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
@@ -11,6 +11,7 @@ using System.Linq;
using System.Threading;
using osu.Game.Rulesets.Osu.UI;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Beatmaps.Legacy;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in ().Sum(s => s.NestedHitObjects.Count - 1);
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
+ int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
return new OsuDifficultyAttributes
@@ -78,6 +79,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
DrainRate = drainRate,
MaxCombo = maxCombo,
HitCircleCount = hitCirclesCount,
+ SliderCount = sliderCount,
SpinnerCount = spinnerCount,
Skills = skills
};
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 4e4dbc02a1..4bca87204a 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private int countMeh;
private int countMiss;
+ private int effectiveMissCount;
+
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
{
@@ -39,19 +41,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
+ effectiveMissCount = calculateEffectiveMissCount();
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
// Custom multipliers for NoFail and SpunOut.
if (mods.Any(m => m is OsuModNoFail))
- multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
+ multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
if (mods.Any(m => m is OsuModSpunOut))
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
if (mods.Any(h => h is OsuModRelax))
{
- countMiss += countOk + countMeh;
+ effectiveMissCount += countOk + countMeh;
multiplier *= 0.6;
}
@@ -97,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
- if (countMiss > 0)
- aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss);
+ if (effectiveMissCount > 0)
+ aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount);
// Combo scaling.
if (Attributes.MaxCombo > 0)
@@ -115,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
if (mods.Any(m => m is OsuModBlinds))
- aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
+ aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * 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.
@@ -142,8 +145,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
- if (countMiss > 0)
- speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
+ if (effectiveMissCount > 0)
+ speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling.
if (Attributes.MaxCombo > 0)
@@ -231,8 +234,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
flashlightValue *= 1.3;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
- if (countMiss > 0)
- flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
+ if (effectiveMissCount > 0)
+ flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling.
if (Attributes.MaxCombo > 0)
@@ -250,6 +253,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
+ private int calculateEffectiveMissCount()
+ {
+ // guess the number of misses + slider breaks from combo
+ double comboBasedMissCount = 0.0;
+
+ if (Attributes.SliderCount > 0)
+ {
+ double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount;
+ if (scoreMaxCombo < fullComboThreshold)
+ comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
+ }
+
+ // we're clamping misscount because since its derived from combo it can be higher than total hits and that breaks some calculations
+ comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
+
+ return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount));
+ }
+
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 8e8f9bc06e..5e5993aefe 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -54,6 +54,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
private void setDistances()
{
+ // We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
+ if (BaseObject is Spinner || lastObject is Spinner)
+ return;
+
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalized_radius / (float)BaseObject.Radius;
@@ -71,11 +75,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
- // Don't need to jump to reach spinners
- if (!(BaseObject is Spinner))
- JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
+ JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
- if (lastLastObject != null)
+ if (lastLastObject != null && !(lastLastObject is Spinner))
{
Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastObject);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index b9e4ed6fcb..07b6a1bdc2 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -8,12 +8,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
@@ -67,6 +69,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
inputManager = GetContainingInputManager();
}
+ [Resolved]
+ private EditorBeatmap editorBeatmap { get; set; }
+
public override void UpdateTimeAndPosition(SnapResult result)
{
base.UpdateTimeAndPosition(result);
@@ -75,6 +80,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
case SliderPlacementState.Initial:
BeginPlacement();
+
+ var nearestDifficultyPoint = editorBeatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
+
+ HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
break;
@@ -212,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
- HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
+ HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 89724876fa..a7fadfb67f 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePath()
{
- HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
+ HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
editorBeatmap?.Update(HitObject);
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
index ff3be97427..8a561f962a 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
- : base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime)
+ : base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime)
{
Masking = true;
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
new file mode 100644
index 0000000000..c48cbd9992
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
@@ -0,0 +1,76 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.Mods;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Bindables;
+using osu.Framework.Localisation;
+using osu.Framework.Utils;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Configuration;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModNoScope : Mod, IUpdatableByPlayfield, IApplicableToScoreProcessor
+ {
+ ///
+ /// Slightly higher than the cutoff for .
+ ///
+ private const float min_alpha = 0.0002f;
+
+ private const float transition_duration = 100;
+
+ public override string Name => "No Scope";
+ public override string Acronym => "NS";
+ public override ModType Type => ModType.Fun;
+ public override IconUsage? Icon => FontAwesome.Solid.EyeSlash;
+ public override string Description => "Where's the cursor?";
+ public override double ScoreMultiplier => 1;
+
+ private BindableNumber currentCombo;
+
+ private float targetAlpha;
+
+ [SettingSource(
+ "Hidden at combo",
+ "The combo count at which the cursor becomes completely hidden",
+ SettingControlType = typeof(SettingsSlider)
+ )]
+ public BindableInt HiddenComboCount { get; } = new BindableInt
+ {
+ Default = 10,
+ Value = 10,
+ MinValue = 0,
+ MaxValue = 50,
+ };
+
+ public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
+
+ public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
+ {
+ if (HiddenComboCount.Value == 0) return;
+
+ currentCombo = scoreProcessor.Combo.GetBoundCopy();
+ currentCombo.BindValueChanged(combo =>
+ {
+ targetAlpha = Math.Max(min_alpha, 1 - (float)combo.NewValue / HiddenComboCount.Value);
+ }, true);
+ }
+
+ public virtual void Update(Playfield playfield)
+ {
+ playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / transition_duration, 0, 1));
+ }
+ }
+
+ public class HiddenComboSlider : OsuSliderBar
+ {
+ public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 1d2666f46b..07d03ee1eb 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -140,9 +140,8 @@ namespace osu.Game.Rulesets.Osu.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
@@ -175,7 +174,6 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
- SampleControlPoint = SampleControlPoint,
});
break;
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index f4a93a571d..ee4712c3b8 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -192,6 +192,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModBarrelRoll(),
new OsuModApproachDifferent(),
new OsuModMuted(),
+ new OsuModNoScope(),
};
case ModType.System:
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 50c0ca7f55..32aad6c36a 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
- DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
+ DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint;
double beatLength;
#pragma warning disable 618
@@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
- beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
+ beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index 0318e32991..0e93ad7e73 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -63,9 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = base_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
tickSpacing = timingPoint.BeatLength / TickRate;
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index b5e1fa204f..cb12d03620 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -192,15 +192,15 @@ namespace osu.Game.Tests.Beatmaps.Formats
var difficultyPoint = controlPoints.DifficultyPointAt(0);
Assert.AreEqual(0, difficultyPoint.Time);
- Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
+ Assert.AreEqual(1.0, difficultyPoint.SliderVelocity);
difficultyPoint = controlPoints.DifficultyPointAt(48428);
Assert.AreEqual(0, difficultyPoint.Time);
- Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
+ Assert.AreEqual(1.0, difficultyPoint.SliderVelocity);
difficultyPoint = controlPoints.DifficultyPointAt(116999);
Assert.AreEqual(116999, difficultyPoint.Time);
- Assert.AreEqual(0.75, difficultyPoint.SpeedMultiplier, 0.1);
+ Assert.AreEqual(0.75, difficultyPoint.SliderVelocity, 0.1);
var soundPoint = controlPoints.SamplePointAt(0);
Assert.AreEqual(956, soundPoint.Time);
@@ -227,7 +227,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(effectPoint.KiaiMode);
Assert.IsFalse(effectPoint.OmitFirstBarLine);
- effectPoint = controlPoints.EffectPointAt(119637);
+ effectPoint = controlPoints.EffectPointAt(116637);
Assert.AreEqual(95901, effectPoint.Time);
Assert.IsFalse(effectPoint.KiaiMode);
Assert.IsFalse(effectPoint.OmitFirstBarLine);
@@ -249,10 +249,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(3));
Assert.That(controlPoints.SamplePoints.Count, Is.EqualTo(3));
- Assert.That(controlPoints.DifficultyPointAt(500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(1500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(2500).SpeedMultiplier, Is.EqualTo(0.75).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(3500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(1500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(2500).SliderVelocity, Is.EqualTo(0.75).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(3500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
Assert.That(controlPoints.EffectPointAt(500).KiaiMode, Is.True);
Assert.That(controlPoints.EffectPointAt(1500).KiaiMode, Is.True);
@@ -279,10 +279,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("timingpoint-speedmultiplier-reset.osu"))
using (var stream = new LineBufferedReader(resStream))
{
- var controlPoints = decoder.Decode(stream).ControlPointInfo;
+ var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
- Assert.That(controlPoints.DifficultyPointAt(0).SpeedMultiplier, Is.EqualTo(0.5).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(0).SliderVelocity, Is.EqualTo(0.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1).Within(0.1));
}
}
@@ -394,12 +394,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("controlpoint-difficulty-multiplier.osu"))
using (var stream = new LineBufferedReader(resStream))
{
- var controlPointInfo = decoder.Decode(stream).ControlPointInfo;
+ var controlPointInfo = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
- Assert.That(controlPointInfo.DifficultyPointAt(5).SpeedMultiplier, Is.EqualTo(1));
- Assert.That(controlPointInfo.DifficultyPointAt(1000).SpeedMultiplier, Is.EqualTo(10));
- Assert.That(controlPointInfo.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1.8518518518518519d));
- Assert.That(controlPointInfo.DifficultyPointAt(3000).SpeedMultiplier, Is.EqualTo(0.5));
+ Assert.That(controlPointInfo.DifficultyPointAt(5).SliderVelocity, Is.EqualTo(1));
+ Assert.That(controlPointInfo.DifficultyPointAt(1000).SliderVelocity, Is.EqualTo(10));
+ Assert.That(controlPointInfo.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1.8518518518518519d));
+ Assert.That(controlPointInfo.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(0.5));
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index 896aa53f82..d12da1a22f 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -46,8 +46,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
sort(decoded.beatmap);
sort(decodedAfterEncode.beatmap);
- Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
- Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
+ compareBeatmaps(decoded, decodedAfterEncode);
}
[TestCaseSource(nameof(allBeatmaps))]
@@ -62,8 +61,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
sort(decoded.beatmap);
sort(decodedAfterEncode.beatmap);
- Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
- Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
+ compareBeatmaps(decoded, decodedAfterEncode);
}
[TestCaseSource(nameof(allBeatmaps))]
@@ -77,12 +75,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
- // in this process, we may lose some detail in the control points section.
- // let's focus on only the hitobjects.
- var originalHitObjects = decoded.beatmap.HitObjects.Serialize();
- var newHitObjects = decodedAfterEncode.beatmap.HitObjects.Serialize();
-
- Assert.That(newHitObjects, Is.EqualTo(originalHitObjects));
+ compareBeatmaps(decoded, decodedAfterEncode);
ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
{
@@ -97,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
// completely ignore "legacy" types, which have been moved to HitObjects.
// even though these would mostly be ignored by the Add call, they will still be available in groups,
// which isn't what we want to be testing here.
- if (point is SampleControlPoint)
+ if (point is SampleControlPoint || point is DifficultyControlPoint)
continue;
newControlPoints.Add(point.Time, point.DeepClone());
@@ -107,6 +100,19 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ private void compareBeatmaps((IBeatmap beatmap, TestLegacySkin skin) expected, (IBeatmap beatmap, TestLegacySkin skin) actual)
+ {
+ // Check all control points that are still considered to be at a global level.
+ Assert.That(expected.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.TimingPoints.Serialize()));
+ Assert.That(expected.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.EffectPoints.Serialize()));
+
+ // Check all hitobjects.
+ Assert.That(expected.beatmap.HitObjects.Serialize(), Is.EqualTo(actual.beatmap.HitObjects.Serialize()));
+
+ // Check skin.
+ Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration));
+ }
+
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{
@@ -156,7 +162,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
- private (IBeatmap beatmap, TestLegacySkin beatmapSkin) decodeFromLegacy(Stream stream, string name)
+ private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name)
{
using (var reader = new LineBufferedReader(stream))
{
@@ -174,7 +180,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
- private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
+ private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin skin) fullBeatmap)
{
var (beatmap, beatmapSkin) = fullBeatmap;
var stream = new MemoryStream();
diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index 2c2c4dc24e..af87fc17ad 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -509,5 +509,17 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(LinkAction.External, result.Action);
Assert.AreEqual("/relative", result.Argument);
}
+
+ [TestCase("https://dev.ppy.sh/home/changelog", "")]
+ [TestCase("https://dev.ppy.sh/home/changelog/lazer/2021.1012", "lazer/2021.1012")]
+ public void TestChangelogLinks(string link, string expectedArg)
+ {
+ MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
+
+ LinkDetails result = MessageFormatter.GetLinkDetails(link);
+
+ Assert.AreEqual(LinkAction.OpenChangelog, result.Action);
+ Assert.AreEqual(expectedArg, result.Argument);
+ }
}
}
diff --git a/osu.Game.Tests/Database/FileStoreTests.cs b/osu.Game.Tests/Database/FileStoreTests.cs
new file mode 100644
index 0000000000..861de5303d
--- /dev/null
+++ b/osu.Game.Tests/Database/FileStoreTests.cs
@@ -0,0 +1,114 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Logging;
+using osu.Game.Models;
+using osu.Game.Stores;
+
+#nullable enable
+
+namespace osu.Game.Tests.Database
+{
+ public class FileStoreTests : RealmTest
+ {
+ [Test]
+ public void TestImportFile()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
+
+ realm.Write(() => files.Add(testData, realm));
+
+ Assert.True(files.Storage.Exists("0/05/054edec1d0211f624fed0cbca9d4f9400b0e491c43742af2c5b0abebf0c990d8"));
+ Assert.True(files.Storage.Exists(realm.All().First().StoragePath));
+ });
+ }
+
+ [Test]
+ public void TestImportSameFileTwice()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
+
+ realm.Write(() => files.Add(testData, realm));
+ realm.Write(() => files.Add(testData, realm));
+
+ Assert.AreEqual(1, realm.All().Count());
+ });
+ }
+
+ [Test]
+ public void TestDontPurgeReferenced()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
+
+ var timer = new Stopwatch();
+ timer.Start();
+
+ realm.Write(() =>
+ {
+ // attach the file to an arbitrary beatmap
+ var beatmapSet = CreateBeatmapSet(CreateRuleset());
+
+ beatmapSet.Files.Add(new RealmNamedFileUsage(file, "arbitrary.resource"));
+
+ realm.Add(beatmapSet);
+ });
+
+ Logger.Log($"Import complete at {timer.ElapsedMilliseconds}");
+
+ string path = file.StoragePath;
+
+ Assert.True(realm.All().Any());
+ Assert.True(files.Storage.Exists(path));
+
+ files.Cleanup();
+ Logger.Log($"Cleanup complete at {timer.ElapsedMilliseconds}");
+
+ Assert.True(realm.All().Any());
+ Assert.True(file.IsValid);
+ Assert.True(files.Storage.Exists(path));
+ });
+ }
+
+ [Test]
+ public void TestPurgeUnreferenced()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
+
+ string path = file.StoragePath;
+
+ Assert.True(realm.All().Any());
+ Assert.True(files.Storage.Exists(path));
+
+ files.Cleanup();
+
+ Assert.False(realm.All().Any());
+ Assert.False(file.IsValid);
+ Assert.False(files.Storage.Exists(path));
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs
index 245981cd9b..3e8b6091fd 100644
--- a/osu.Game.Tests/Database/GeneralUsageTests.cs
+++ b/osu.Game.Tests/Database/GeneralUsageTests.cs
@@ -1,3 +1,6 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
using System;
using System.Threading;
using System.Threading.Tasks;
diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs
new file mode 100644
index 0000000000..33aa1afb89
--- /dev/null
+++ b/osu.Game.Tests/Database/RealmLiveTests.cs
@@ -0,0 +1,213 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Models;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Tests.Database
+{
+ public class RealmLiveTests : RealmTest
+ {
+ [Test]
+ public void TestLiveCastability()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
+
+ ILive iBeatmap = beatmap;
+
+ Assert.AreEqual(0, iBeatmap.Value.Length);
+ });
+ }
+
+ [Test]
+ public void TestValueAccessWithOpenContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ using (realmFactory.CreateContext())
+ {
+ var resolved = liveBeatmap.Value;
+
+ Assert.IsTrue(resolved.Realm.IsClosed);
+ Assert.IsTrue(resolved.IsValid);
+
+ // can access properties without a crash.
+ Assert.IsFalse(resolved.Hidden);
+ }
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestScopedReadWithoutContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ liveBeatmap.PerformRead(beatmap =>
+ {
+ Assert.IsTrue(beatmap.IsValid);
+ Assert.IsFalse(beatmap.Hidden);
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestScopedWriteWithoutContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; });
+ liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestValueAccessWithoutOpenContextFails()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ Assert.Throws(() =>
+ {
+ var unused = liveBeatmap.Value;
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestLiveAssumptions()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ int changesTriggered = 0;
+
+ using (var updateThreadContext = realmFactory.CreateContext())
+ {
+ updateThreadContext.All().SubscribeForNotifications(gotChange);
+ RealmLive? liveBeatmap = null;
+
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var ruleset = CreateRuleset();
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ // add a second beatmap to ensure that a full refresh occurs below.
+ // not just a refresh from the resolved Live.
+ threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ // not yet seen by main context
+ Assert.AreEqual(0, updateThreadContext.All().Count());
+ Assert.AreEqual(0, changesTriggered);
+
+ var resolved = liveBeatmap.Value;
+
+ // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
+ Assert.AreEqual(2, updateThreadContext.All().Count());
+ Assert.AreEqual(1, changesTriggered);
+
+ // even though the realm that this instance was resolved for was closed, it's still valid.
+ Assert.IsTrue(resolved.Realm.IsClosed);
+ Assert.IsTrue(resolved.IsValid);
+
+ // can access properties without a crash.
+ Assert.IsFalse(resolved.Hidden);
+
+ updateThreadContext.Write(r =>
+ {
+ // can use with the main context.
+ r.Remove(resolved);
+ });
+ }
+
+ void gotChange(IRealmCollection sender, ChangeSet changes, Exception error)
+ {
+ changesTriggered++;
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs
index 576f901c1a..04c9f2577a 100644
--- a/osu.Game.Tests/Database/RealmTest.cs
+++ b/osu.Game.Tests/Database/RealmTest.cs
@@ -4,12 +4,13 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
-using Nito.AsyncEx;
using NUnit.Framework;
+using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Database;
+using osu.Game.Models;
#nullable enable
@@ -28,42 +29,109 @@ namespace osu.Game.Tests.Database
protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "")
{
- AsyncContext.Run(() =>
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{
- var testStorage = storage.GetStorageForDirectory(caller);
-
- using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ host.Run(new RealmTestGame(() =>
{
- Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
- testAction(realmFactory, testStorage);
+ var testStorage = storage.GetStorageForDirectory(caller);
- realmFactory.Dispose();
+ using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ {
+ Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
+ testAction(realmFactory, testStorage);
- Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
- realmFactory.Compact();
- Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
- }
- });
+ realmFactory.Dispose();
+
+ Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
+ realmFactory.Compact();
+ Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
+ }
+ }));
+ }
}
protected void RunTestWithRealmAsync(Func testAction, [CallerMemberName] string caller = "")
{
- AsyncContext.Run(async () =>
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{
- var testStorage = storage.GetStorageForDirectory(caller);
-
- using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ host.Run(new RealmTestGame(async () =>
{
- Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
- await testAction(realmFactory, testStorage);
+ var testStorage = storage.GetStorageForDirectory(caller);
- realmFactory.Dispose();
+ using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ {
+ Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
+ await testAction(realmFactory, testStorage);
- Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
- realmFactory.Compact();
- Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
+ realmFactory.Dispose();
+
+ Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
+ realmFactory.Compact();
+ }
+ }));
+ }
+ }
+
+ protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset)
+ {
+ RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() };
+
+ var metadata = new RealmBeatmapMetadata
+ {
+ Title = "My Love",
+ Artist = "Kuba Oms"
+ };
+
+ var beatmapSet = new RealmBeatmapSet
+ {
+ Beatmaps =
+ {
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", }
+ },
+ Files =
+ {
+ new RealmNamedFileUsage(createRealmFile(), "test [easy].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [normal].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [hard].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [insane].osu"),
}
- });
+ };
+
+ for (int i = 0; i < 8; i++)
+ beatmapSet.Files.Add(new RealmNamedFileUsage(createRealmFile(), $"hitsound{i}.mp3"));
+
+ foreach (var b in beatmapSet.Beatmaps)
+ b.BeatmapSet = beatmapSet;
+
+ return beatmapSet;
+ }
+
+ protected static RealmRuleset CreateRuleset() =>
+ new RealmRuleset(0, "osu!", "osu", true);
+
+ private class RealmTestGame : Framework.Game
+ {
+ public RealmTestGame(Func work)
+ {
+ // ReSharper disable once AsyncVoidLambda
+ Scheduler.Add(async () =>
+ {
+ await work().ConfigureAwait(true);
+ Exit();
+ });
+ }
+
+ public RealmTestGame(Action work)
+ {
+ Scheduler.Add(() =>
+ {
+ work();
+ Exit();
+ });
+ }
}
private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory)
diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs
new file mode 100644
index 0000000000..f4e0838be1
--- /dev/null
+++ b/osu.Game.Tests/Database/RulesetStoreTests.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Models;
+using osu.Game.Stores;
+
+namespace osu.Game.Tests.Database
+{
+ public class RulesetStoreTests : RealmTest
+ {
+ [Test]
+ public void TestCreateStore()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var rulesets = new RealmRulesetStore(realmFactory, storage);
+
+ Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
+ Assert.AreEqual(4, realmFactory.Context.All().Count());
+ });
+ }
+
+ [Test]
+ public void TestCreateStoreTwiceDoesntAddRulesetsAgain()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var rulesets = new RealmRulesetStore(realmFactory, storage);
+ var rulesets2 = new RealmRulesetStore(realmFactory, storage);
+
+ Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
+ Assert.AreEqual(4, rulesets2.AvailableRulesets.Count());
+
+ Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First());
+ Assert.AreEqual(4, realmFactory.Context.All().Count());
+ });
+ }
+
+ [Test]
+ public void TestRetrievedRulesetsAreDetached()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var rulesets = new RealmRulesetStore(realmFactory, storage);
+
+ Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false);
+ Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false);
+ Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
new file mode 100644
index 0000000000..f3a4f10210
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
@@ -0,0 +1,104 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Storyboards;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckAudioInVideoTest
+ {
+ private CheckAudioInVideo check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckAudioInVideo();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.mp4",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestRegularVideoFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4"))
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+
+ [Test]
+ public void TestVideoFileWithAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-audio.mp4"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
+ }
+ }
+
+ [Test]
+ public void TestVideoFileWithTrackButNoAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-track-but-no-audio.mp4"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
+ }
+ }
+
+ [Test]
+ public void TestMissingFile()
+ {
+ beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
+
+ var issues = check.Run(getContext(null)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateMissingFile);
+ }
+
+ private BeatmapVerifierContext getContext(Stream resourceStream)
+ {
+ var storyboard = new Storyboard();
+ var layer = storyboard.GetLayer("Video");
+ layer.Add(new StoryboardVideo("abc123.mp4", 0));
+
+ var mockWorkingBeatmap = new Mock(beatmap, null, null);
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream);
+ mockWorkingBeatmap.As().SetupGet(w => w.Storyboard).Returns(storyboard);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
new file mode 100644
index 0000000000..9b090591bc
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
@@ -0,0 +1,128 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using ManagedBass;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+using osuTK.Audio;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckTooShortAudioFilesTest
+ {
+ private CheckTooShortAudioFiles check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckTooShortAudioFiles();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.wav",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+
+ // 0 = No output device. This still allows decoding.
+ if (!Bass.Init(0) && Bass.LastError != Errors.Already)
+ throw new AudioException("Could not initialize Bass.");
+ }
+
+ [Test]
+ public void TestDifferentExtension()
+ {
+ beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
+ beatmap.BeatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo
+ {
+ Filename = "abc123.jpg",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ });
+
+ // Should fail to load, but not produce an error due to the extension not being expected to load.
+ Assert.IsEmpty(check.Run(getContext(null, allowMissing: true)));
+ }
+
+ [Test]
+ public void TestRegularAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample.mp3"))
+ {
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+ }
+
+ [Test]
+ public void TestBlankAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/blank.wav"))
+ {
+ // This is a 0 ms duration audio file, commonly used to silence sliderslides/ticks, and so should be fine.
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+ }
+
+ [Test]
+ public void TestTooShortAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateTooShort);
+ }
+ }
+
+ [Test]
+ public void TestMissingAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3"))
+ {
+ Assert.IsEmpty(check.Run(getContext(resourceStream, allowMissing: true)));
+ }
+ }
+
+ [Test]
+ public void TestCorruptAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat);
+ }
+ }
+
+ private BeatmapVerifierContext getContext(Stream resourceStream, bool allowMissing = false)
+ {
+ var mockWorkingBeatmap = new Mock(beatmap, null, null);
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs
new file mode 100644
index 0000000000..c9adc030c1
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs
@@ -0,0 +1,86 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckZeroByteFilesTest
+ {
+ private CheckZeroByteFiles check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckZeroByteFiles();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.jpg",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestNonZeroBytes()
+ {
+ Assert.IsEmpty(check.Run(getContext(byteLength: 44)));
+ }
+
+ [Test]
+ public void TestZeroBytes()
+ {
+ var issues = check.Run(getContext(byteLength: 0)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckZeroByteFiles.IssueTemplateZeroBytes);
+ }
+
+ [Test]
+ public void TestMissing()
+ {
+ Assert.IsEmpty(check.Run(getContextMissing()));
+ }
+
+ private BeatmapVerifierContext getContext(long byteLength)
+ {
+ var mockStream = new Mock();
+ mockStream.Setup(s => s.Length).Returns(byteLength);
+
+ var mockWorkingBeatmap = new Mock();
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(mockStream.Object);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+
+ private BeatmapVerifierContext getContextMissing()
+ {
+ var mockWorkingBeatmap = new Mock();
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns((Stream)null);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index a40a6dac4c..8eb9452736 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
@@ -55,8 +56,6 @@ namespace osu.Game.Tests.Editing
composer.EditorBeatmap.Difficulty.SliderMultiplier = 1;
composer.EditorBeatmap.ControlPointInfo.Clear();
-
- composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 });
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 });
});
@@ -73,13 +72,13 @@ namespace osu.Game.Tests.Editing
[TestCase(2)]
public void TestSpeedMultiplier(float multiplier)
{
- AddStep($"set multiplier = {multiplier}", () =>
+ assertSnapDistance(100 * multiplier, new HitObject
{
- composer.EditorBeatmap.ControlPointInfo.Clear();
- composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = multiplier });
+ DifficultyControlPoint = new DifficultyControlPoint
+ {
+ SliderVelocity = multiplier
+ }
});
-
- assertSnapDistance(100 * multiplier);
}
[TestCase(1)]
@@ -197,20 +196,20 @@ namespace osu.Game.Tests.Editing
assertSnappedDistance(400, 400);
}
- private void assertSnapDistance(float expectedDistance)
- => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(0) == expectedDistance);
+ private void assertSnapDistance(float expectedDistance, HitObject hitObject = null)
+ => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()) == expectedDistance);
private void assertDurationToDistance(double duration, float expectedDistance)
- => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(0, duration) == expectedDistance);
+ => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance);
private void assertDistanceToDuration(float distance, double expectedDuration)
- => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(0, distance) == expectedDuration);
+ => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration);
private void assertSnappedDuration(float distance, double expectedDuration)
- => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(0, distance) == expectedDuration);
+ => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(new HitObject(), distance) == expectedDuration);
private void assertSnappedDistance(float distance, float expectedDistance)
- => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(0, distance) == expectedDistance);
+ => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(new HitObject(), distance) == expectedDistance);
private class TestHitObjectComposer : OsuHitObjectComposer
{
diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs
index e888f51e98..dbeb453d4d 100644
--- a/osu.Game.Tests/ImportTest.cs
+++ b/osu.Game.Tests/ImportTest.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Tests
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
{
var osu = new TestOsuGameBase(withBeatmap);
- Task.Run(() => host.Run(osu))
+ Task.Factory.StartNew(() => host.Run(osu), TaskCreationOptions.LongRunning)
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
index fabb016d5f..cfda4f6422 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddRedundantDifficulty()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
cpi.Add(0, new DifficultyControlPoint()); // is redundant
cpi.Add(1000, new DifficultyControlPoint()); // is redundant
@@ -55,7 +55,7 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0));
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
- cpi.Add(1000, new DifficultyControlPoint { SpeedMultiplier = 2 }); // is not redundant
+ cpi.Add(1000, new DifficultyControlPoint { SliderVelocity = 2 }); // is not redundant
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
@@ -159,7 +159,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddControlPointToGroup()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
@@ -174,23 +174,23 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddDuplicateControlPointToGroup()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
group.Add(new DifficultyControlPoint());
- group.Add(new DifficultyControlPoint { SpeedMultiplier = 2 });
+ group.Add(new DifficultyControlPoint { SliderVelocity = 2 });
Assert.That(group.ControlPoints.Count, Is.EqualTo(1));
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
- Assert.That(cpi.DifficultyPoints.First().SpeedMultiplier, Is.EqualTo(2));
+ Assert.That(cpi.DifficultyPoints.First().SliderVelocity, Is.EqualTo(2));
}
[Test]
public void TestRemoveControlPointFromGroup()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
@@ -208,14 +208,14 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestOrdering()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
cpi.Add(0, new TimingControlPoint());
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
- cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
- cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
+ cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 });
+ cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 });
cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
@@ -230,14 +230,14 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestClear()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
cpi.Add(0, new TimingControlPoint());
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
- cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
- cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
+ cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 });
+ cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 });
cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index 79767bc671..558b874234 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -168,14 +168,14 @@ namespace osu.Game.Tests.Online
return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host);
}
- protected override BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host)
+ protected override BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager manager, IAPIProvider api, GameHost host)
{
- return new TestBeatmapModelDownloader(modelManager, api, host);
+ return new TestBeatmapModelDownloader(manager, api, host);
}
internal class TestBeatmapModelDownloader : BeatmapModelDownloader
{
- public TestBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost)
+ public TestBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost)
: base(modelManager, apiProvider, gameHost)
{
}
diff --git a/osu.Game.Tests/Resources/Samples/blank.wav b/osu.Game.Tests/Resources/Samples/blank.wav
new file mode 100644
index 0000000000..878bf23cea
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/blank.wav differ
diff --git a/osu.Game.Tests/Resources/Samples/corrupt.wav b/osu.Game.Tests/Resources/Samples/corrupt.wav
new file mode 100644
index 0000000000..87c7de4b7b
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/corrupt.wav differ
diff --git a/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3
new file mode 100644
index 0000000000..003fe23dca
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4
new file mode 100644
index 0000000000..5d380ab50c
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4
new file mode 100644
index 0000000000..7cdd1939e9
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video.mp4 b/osu.Game.Tests/Resources/Videos/test-video.mp4
new file mode 100644
index 0000000000..795483c096
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video.mp4 differ
diff --git a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
index ab47067411..ffb3d41d18 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
@@ -6,7 +6,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
@@ -65,10 +64,9 @@ namespace osu.Game.Tests.Skins
public new void TriggerSourceChanged() => base.TriggerSourceChanged();
- protected override void OnSourceChanged()
+ protected override void RefreshSources()
{
- ResetSources();
- sources.ForEach(AddSource);
+ SetSources(sources);
}
}
diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
index 211543a881..99be72e958 100644
--- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
+++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -18,16 +19,19 @@ namespace osu.Game.Tests.Visual.Audio
{
public class TestSceneAudioFilter : OsuTestScene
{
- private OsuSpriteText lowpassText;
- private AudioFilter lowpassFilter;
+ private OsuSpriteText lowPassText;
+ private AudioFilter lowPassFilter;
- private OsuSpriteText highpassText;
- private AudioFilter highpassFilter;
+ private OsuSpriteText highPassText;
+ private AudioFilter highPassFilter;
private Track track;
private WaveformTestBeatmap beatmap;
+ private OsuSliderBar lowPassSlider;
+ private OsuSliderBar highPassSlider;
+
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
@@ -38,53 +42,89 @@ namespace osu.Game.Tests.Visual.Audio
{
Children = new Drawable[]
{
- lowpassFilter = new AudioFilter(audio.TrackMixer),
- highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
- lowpassText = new OsuSpriteText
+ lowPassFilter = new AudioFilter(audio.TrackMixer),
+ highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
+ lowPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
- Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz",
+ Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
- new OsuSliderBar
+ lowPassSlider = new OsuSliderBar
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
- Current = { BindTarget = lowpassFilter.Cutoff }
+ Current = new BindableInt
+ {
+ MinValue = 0,
+ MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
+ }
},
- highpassText = new OsuSpriteText
+ highPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
- Text = $"High Pass: {highpassFilter.Cutoff.Value}hz",
+ Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
- new OsuSliderBar
+ highPassSlider = new OsuSliderBar
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
- Current = { BindTarget = highpassFilter.Cutoff }
+ Current = new BindableInt
+ {
+ MinValue = 0,
+ MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
+ }
}
}
});
- lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz";
- highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz";
+
+ lowPassSlider.Current.ValueChanged += e =>
+ {
+ lowPassText.Text = $"Low Pass: {e.NewValue}hz";
+ lowPassFilter.Cutoff = e.NewValue;
+ };
+
+ highPassSlider.Current.ValueChanged += e =>
+ {
+ highPassText.Text = $"High Pass: {e.NewValue}hz";
+ highPassFilter.Cutoff = e.NewValue;
+ };
}
+ #region Overrides of Drawable
+
+ protected override void Update()
+ {
+ base.Update();
+ highPassSlider.Current.Value = highPassFilter.Cutoff;
+ lowPassSlider.Current.Value = lowPassFilter.Cutoff;
+ }
+
+ #endregion
+
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Play Track", () => track.Start());
+
+ AddStep("Reset filters", () =>
+ {
+ lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF;
+ highPassFilter.Cutoff = 0;
+ });
+
waitTrackPlay();
}
[Test]
- public void TestLowPass()
+ public void TestLowPassSweep()
{
AddStep("Filter Sweep", () =>
{
- lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
@@ -92,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
- lowpassFilter.CutoffTo(0).Then()
+ lowPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@@ -101,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio
}
[Test]
- public void TestHighPass()
+ public void TestHighPassSweep()
{
AddStep("Filter Sweep", () =>
{
- highpassFilter.CutoffTo(0).Then()
+ highPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@@ -113,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
- highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
+ highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
@@ -123,5 +163,11 @@ namespace osu.Game.Tests.Visual.Audio
}
private void waitTrackPlay() => AddWaitStep("Let track play", 10);
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ track?.Dispose();
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
index 11830ebe35..d1efd22d6f 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
@@ -81,7 +82,7 @@ namespace osu.Game.Tests.Visual.Editing
public new float DistanceSpacing => base.DistanceSpacing;
public TestDistanceSnapGrid(double? endTime = null)
- : base(grid_position, 0, endTime)
+ : base(new HitObject(), grid_position, 0, endTime)
{
}
@@ -158,15 +159,15 @@ namespace osu.Game.Tests.Visual.Editing
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
- public float GetBeatSnapDistanceAt(double referenceTime) => 10;
+ public float GetBeatSnapDistanceAt(HitObject referenceObject) => 10;
- public float DurationToDistance(double referenceTime, double duration) => (float)duration;
+ public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
- public double DistanceToDuration(double referenceTime, float distance) => distance;
+ public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
- public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
+ public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
- public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
+ public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
}
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
index 2258a209e2..ab2bc4649a 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -8,6 +8,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osuTK.Input;
@@ -30,22 +31,36 @@ namespace osu.Game.Tests.Visual.Editing
PushAndConfirm(() => new EditorLoader());
- AddUntilStep("wait for editor load", () => editor != null);
+ AddUntilStep("wait for editor load", () => editor?.IsLoaded == true);
- AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
+ AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
+
+ // We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten.
AddStep("Enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
+ AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7);
+ AddStep("Set artist and title", () =>
+ {
+ editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
+ editorBeatmap.BeatmapInfo.Metadata.Title = "title";
+ });
+ AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.Version = "difficulty");
+
+ AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
+
AddStep("Change to placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left));
- AddStep("Save and exit", () =>
- {
- InputManager.Keys(PlatformAction.Save);
- InputManager.Key(Key.Escape);
- });
+ checkMutations();
+
+ AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
+
+ checkMutations();
+
+ AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@@ -56,7 +71,16 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
AddUntilStep("Wait for editor load", () => editor != null);
+
+ checkMutations();
+ }
+
+ private void checkMutations()
+ {
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
+ AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7);
+ AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
+ AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.Version == "difficulty");
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
index 00b5c38e20..c5ab3974a4 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
@@ -20,14 +20,15 @@ namespace osu.Game.Tests.Visual.Gameplay
///
public abstract class TestSceneAllRulesetPlayers : RateAdjustedBeatmapTestScene
{
- protected Player Player;
+ protected Player Player { get; private set; }
+
+ protected OsuConfigManager Config { get; private set; }
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
- OsuConfigManager manager;
- Dependencies.Cache(manager = new OsuConfigManager(LocalStorage));
- manager.GetBindable(OsuSetting.DimLevel).Value = 1.0;
+ Dependencies.Cache(Config = new OsuConfigManager(LocalStorage));
+ Config.GetBindable(OsuSetting.DimLevel).Value = 1.0;
}
[Test]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
index 85aaf20a19..36fc6812bd 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
@@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using NUnit.Framework;
using osu.Framework.Graphics.Containers;
+using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play;
@@ -17,6 +19,14 @@ namespace osu.Game.Tests.Visual.Gameplay
return new FailPlayer();
}
+ [Test]
+ public void TestOsuWithoutRedTint()
+ {
+ AddStep("Disable red tint", () => Config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false));
+ TestOsu();
+ AddStep("Enable red tint", () => Config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true));
+ }
+
protected override void AddCheckSteps()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
index 5eb71e92c2..ae0decaee1 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
@@ -103,6 +103,30 @@ namespace osu.Game.Tests.Visual.Gameplay
checkFrameCount(0);
}
+ [Test]
+ public void TestRatePreservedWhenTimeNotProgressing()
+ {
+ AddStep("set manual clock rate", () => manualClock.Rate = 1);
+ seekManualTo(5000);
+ createStabilityContainer();
+ checkRate(1);
+
+ seekManualTo(10000);
+ checkRate(1);
+
+ AddWaitStep("wait some", 3);
+ checkRate(1);
+
+ seekManualTo(5000);
+ checkRate(-1);
+
+ AddWaitStep("wait some", 3);
+ checkRate(-1);
+
+ seekManualTo(10000);
+ checkRate(1);
+ }
+
private const int max_frames_catchup = 50;
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () =>
@@ -116,6 +140,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private void checkFrameCount(int frames) =>
AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames);
+ private void checkRate(double rate) =>
+ AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate);
+
public class ClockConsumingChild : CompositeDrawable
{
private readonly OsuSpriteText text;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index aee15a145c..ba0ee5ac6e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -291,7 +291,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
- AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType().Any() == warning);
+ AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => (getWarning() != null) == warning);
if (warning)
{
@@ -335,12 +335,17 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
- AddUntilStep("wait for epilepsy warning", () => loader.ChildrenOfType().Single().Alpha > 0);
+ AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0);
+ AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible);
+
AddStep("exit early", () => loader.Exit());
+ AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden);
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1);
}
+ private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault();
+
private class TestPlayerLoader : PlayerLoader
{
public new VisualSettings VisualSettings => base.VisualSettings;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
index 5ff2e9c439..bf864f844c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
@@ -10,10 +11,13 @@ using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko;
+using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
@@ -32,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool HasCustomSteps => true;
- protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new NonImportingPlayer(false);
protected override Ruleset CreatePlayerRuleset() => createCustomRuleset?.Invoke() ?? new OsuRuleset();
@@ -86,6 +90,46 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
}
+ [Test]
+ public void TestSubmissionForDifferentRuleset()
+ {
+ prepareTokenResponse(true);
+
+ createPlayerTest(createRuleset: () => new TaikoRuleset());
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ addFakeHit();
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+ AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
+ AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.RulesetID == new TaikoRuleset().RulesetInfo.ID);
+ }
+
+ [Test]
+ public void TestSubmissionForConvertedBeatmap()
+ {
+ prepareTokenResponse(true);
+
+ createPlayerTest(createRuleset: () => new ManiaRuleset(), createBeatmap: _ => createTestBeatmap(new OsuRuleset().RulesetInfo));
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ addFakeHit();
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+ AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
+ AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.RulesetID == new ManiaRuleset().RulesetInfo.ID);
+ }
+
[Test]
public void TestNoSubmissionOnExitWithNoToken()
{
@@ -183,12 +227,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
- [Test]
- public void TestNoSubmissionOnCustomRuleset()
+ [TestCase(null)]
+ [TestCase(10)]
+ public void TestNoSubmissionOnCustomRuleset(int? rulesetId)
{
prepareTokenResponse(true);
- createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { ID = 10 } });
+ createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { ID = rulesetId } });
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
@@ -242,5 +287,33 @@ namespace osu.Game.Tests.Visual.Gameplay
});
});
}
+
+ private class NonImportingPlayer : TestPlayer
+ {
+ public NonImportingPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
+ : base(allowPause, showResults, pauseOnFocusLost)
+ {
+ }
+
+ protected override Task ImportScore(Score score)
+ {
+ // It was discovered that Score members could sometimes be half-populated.
+ // In particular, the RulesetID property could be set to 0 even on non-osu! maps.
+ // We want to test that the state of that property is consistent in this test.
+ // EF makes this impossible.
+ //
+ // First off, because of the EF navigational property-explicit foreign key field duality,
+ // it can happen that - for example - the Ruleset navigational property is correctly initialised to mania,
+ // but the RulesetID foreign key property is not initialised and remains 0.
+ // EF silently bypasses this by prioritising the Ruleset navigational property over the RulesetID foreign key one.
+ //
+ // Additionally, adding an entity to an EF DbSet CAUSES SIDE EFFECTS with regard to the foreign key property.
+ // In the above instance, if a ScoreInfo with Ruleset = {mania} and RulesetID = 0 is attached to an EF context,
+ // RulesetID WILL BE SILENTLY SET TO THE CORRECT VALUE of 3.
+ //
+ // For the above reasons, importing is disabled in this test.
+ return Task.CompletedTask;
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 2f15e549f7..283fe594ea 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -93,9 +93,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private IList testControlPoints => new List
{
- new MultiplierControlPoint(time_range) { DifficultyPoint = { SpeedMultiplier = 1.25 } },
- new MultiplierControlPoint(1.5 * time_range) { DifficultyPoint = { SpeedMultiplier = 1 } },
- new MultiplierControlPoint(2 * time_range) { DifficultyPoint = { SpeedMultiplier = 1.5 } }
+ new MultiplierControlPoint(time_range) { EffectPoint = { ScrollSpeed = 1.25 } },
+ new MultiplierControlPoint(1.5 * time_range) { EffectPoint = { ScrollSpeed = 1 } },
+ new MultiplierControlPoint(2 * time_range) { EffectPoint = { ScrollSpeed = 1.5 } }
};
[Test]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs
new file mode 100644
index 0000000000..89fea1f92d
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Online.API;
+using osu.Game.Online.Spectator;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Tests.Visual.Spectator;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneSpectatorHost : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ [Cached(typeof(SpectatorClient))]
+ private TestSpectatorClient spectatorClient { get; } = new TestSpectatorClient();
+
+ private DummyAPIAccess dummyAPIAccess => (DummyAPIAccess)API;
+ private const int dummy_user_id = 42;
+
+ public override void SetUpSteps()
+ {
+ AddStep("set dummy user", () => dummyAPIAccess.LocalUser.Value = new User
+ {
+ Id = dummy_user_id,
+ Username = "DummyUser"
+ });
+ AddStep("add test spectator client", () => Add(spectatorClient));
+ AddStep("add watching user", () => spectatorClient.WatchUser(dummy_user_id));
+ base.SetUpSteps();
+ }
+
+ [Test]
+ public void TestClientSendsCorrectRuleset()
+ {
+ AddUntilStep("spectator client sending frames", () => spectatorClient.PlayingUserStates.ContainsKey(dummy_user_id));
+ AddAssert("spectator client sent correct ruleset", () => spectatorClient.PlayingUserStates[dummy_user_id].RulesetID == Ruleset.Value.ID);
+ }
+
+ public override void TearDownSteps()
+ {
+ base.TearDownSteps();
+ AddStep("stop watching user", () => spectatorClient.StopWatchingUser(dummy_user_id));
+ AddStep("remove test spectator client", () => Remove(spectatorClient));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
index 3ed274690e..48a97d54f7 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
@@ -90,8 +90,12 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest(() =>
{
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true);
- AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300);
+
+ // Fail occurs at 164ms with the provided beatmap.
+ // Fail animation runs for 2.5s realtime but the gameplay time change is *variable* due to the frequency transform being applied, so we need a bit of lenience.
+ AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600);
});
+
AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 0b70703870..2bb77395ef 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -564,11 +564,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddRepeatStep("click spectate button", () =>
+ AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("click ready button", () =>
{
- InputManager.MoveMouseTo(this.ChildrenOfType().Single());
+ InputManager.MoveMouseTo(readyButton);
InputManager.Click(MouseButton.Left);
- }, 2);
+ });
+
+ AddUntilStep("wait for player to be ready", () => client.Room?.Users[0].State == MultiplayerUserState.Ready);
+ AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("click start button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
@@ -582,6 +589,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for results", () => Stack.CurrentScreen is ResultsScreen);
}
+ private MultiplayerReadyButton readyButton => this.ChildrenOfType().Single();
+
private void createRoom(Func room)
{
AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index c4ebc13245..d1980b03c7 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -275,6 +275,68 @@ namespace osu.Game.Tests.Visual.Multiplayer
var state = i;
AddStep($"set state: {state}", () => Client.ChangeUserState(0, state));
}
+
+ AddStep("set state: downloading", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.Downloading(0)));
+
+ AddStep("set state: locally available", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()));
+ }
+
+ [Test]
+ public void TestModOverlap()
+ {
+ AddStep("add dummy mods", () =>
+ {
+ Client.ChangeUserMods(new Mod[]
+ {
+ new OsuModNoFail(),
+ new OsuModDoubleTime()
+ });
+ });
+
+ AddStep("add user with mods", () =>
+ {
+ Client.AddUser(new User
+ {
+ Id = 0,
+ Username = "Baka",
+ RulesetsStatistics = new Dictionary
+ {
+ {
+ Ruleset.Value.ShortName,
+ new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
+ }
+ },
+ CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ });
+ Client.ChangeUserMods(0, new Mod[]
+ {
+ new OsuModHardRock(),
+ new OsuModDoubleTime()
+ });
+ });
+
+ AddStep("set 0 ready", () => Client.ChangeState(MultiplayerUserState.Ready));
+
+ AddStep("set 1 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating));
+
+ // Have to set back to idle due to status priority.
+ AddStep("set 0 no map, 1 ready", () =>
+ {
+ Client.ChangeState(MultiplayerUserState.Idle);
+ Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded());
+ Client.ChangeUserState(0, MultiplayerUserState.Ready);
+ });
+
+ AddStep("set 0 downloading", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
+
+ AddStep("set 0 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating));
+
+ AddStep("make both default", () =>
+ {
+ Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable());
+ Client.ChangeUserState(0, MultiplayerUserState.Idle);
+ Client.ChangeState(MultiplayerUserState.Idle);
+ });
}
private void createNewParticipantsList()
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
new file mode 100644
index 0000000000..bd723eeed6
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Overlays.Notifications;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+ public class TestSceneStartupImport : OsuGameTestScene
+ {
+ private string importFilename;
+
+ protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { importFilename });
+
+ public override void SetUpSteps()
+ {
+ AddStep("Prepare import beatmap", () => importFilename = TestResources.GetTestBeatmapForImport());
+
+ base.SetUpSteps();
+ }
+
+ [Test]
+ public void TestImportCreatedNotification()
+ {
+ AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType().Count() == 1);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index 997eac709d..dc5b0e0d77 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -3,6 +3,7 @@
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet;
@@ -21,6 +22,9 @@ namespace osu.Game.Tests.Visual.Settings
private TestTabletHandler tabletHandler;
private TabletSettings settings;
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
[SetUpSteps]
public void SetUpSteps()
{
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
index a5b90e6655..0ae4e0c5dc 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
@@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
- () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)));
+ () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItems().ElementAt(index).ChildrenOfType().Single();
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 067f1cabb4..4811fc979e 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -142,6 +142,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("store selected beatmap", () => selected = Beatmap.Value);
+ AddUntilStep("wait for beatmaps to load", () => songSelect.Carousel.ChildrenOfType().Any());
+
AddStep("select next and enter", () =>
{
InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType()
@@ -599,10 +601,10 @@ namespace osu.Game.Tests.Visual.SongSelect
});
FilterableDifficultyIcon difficultyIcon = null;
- AddStep("Find an icon", () =>
+ AddUntilStep("Find an icon", () =>
{
- difficultyIcon = set.ChildrenOfType()
- .First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
+ return (difficultyIcon = set.ChildrenOfType()
+ .FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex())) != null;
});
AddStep("Click on a difficulty", () =>
@@ -765,10 +767,10 @@ namespace osu.Game.Tests.Visual.SongSelect
});
FilterableGroupedDifficultyIcon groupIcon = null;
- AddStep("Find group icon for different ruleset", () =>
+ AddUntilStep("Find group icon for different ruleset", () =>
{
- groupIcon = set.ChildrenOfType()
- .First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3);
+ return (groupIcon = set.ChildrenOfType()
+ .FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3)) != null;
});
AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
index 393420e700..1b7f65f9a0 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
@@ -1,11 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -19,28 +23,62 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("create component", () =>
{
- LabelledSliderBar component;
+ FillFlowContainer flow;
- Child = new Container
+ Child = flow = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
- Child = component = new LabelledSliderBar
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- Current = new BindableDouble(5)
+ new LabelledSliderBar
{
- MinValue = 0,
- MaxValue = 10,
- Precision = 1,
- }
- }
+ Current = new BindableDouble(5)
+ {
+ MinValue = 0,
+ MaxValue = 10,
+ Precision = 1,
+ },
+ Label = "a sample component",
+ Description = hasDescription ? "this text describes the component" : string.Empty,
+ },
+ },
};
- component.Label = "a sample component";
- component.Description = hasDescription ? "this text describes the component" : string.Empty;
+ foreach (var colour in Enum.GetValues(typeof(OverlayColourScheme)).OfType())
+ {
+ flow.Add(new OverlayColourContainer(colour)
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = new LabelledSliderBar
+ {
+ Current = new BindableDouble(5)
+ {
+ MinValue = 0,
+ MaxValue = 10,
+ Precision = 1,
+ },
+ Label = "a sample component",
+ Description = hasDescription ? "this text describes the component" : string.Empty,
+ }
+ });
+ }
});
}
+
+ private class OverlayColourContainer : Container
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider;
+
+ public OverlayColourContainer(OverlayColourScheme scheme)
+ {
+ colourProvider = new OverlayColourProvider(scheme);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
new file mode 100644
index 0000000000..9ccfba7c74
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.UserInterfaceV2;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneRoundedButton : OsuTestScene
+ {
+ [Test]
+ public void TestBasic()
+ {
+ RoundedButton button = null;
+
+ AddStep("create button", () => Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.DarkGray
+ },
+ button = new RoundedButton
+ {
+ Width = 400,
+ Text = "Test button",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Action = () => { }
+ }
+ }
+ });
+
+ AddToggleStep("toggle disabled", disabled => button.Action = disabled ? (Action)null : () => { });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs
new file mode 100644
index 0000000000..fb04c5bad0
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs
@@ -0,0 +1,68 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Settings;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneSettingsCheckbox : OsuTestScene
+ {
+ [TestCase]
+ public void TestCheckbox()
+ {
+ AddStep("create component", () =>
+ {
+ FillFlowContainer flow;
+
+ Child = flow = new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 500,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(5),
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new SettingsCheckbox
+ {
+ LabelText = "a sample component",
+ },
+ },
+ };
+
+ foreach (var colour1 in Enum.GetValues(typeof(OverlayColourScheme)).OfType())
+ {
+ flow.Add(new OverlayColourContainer(colour1)
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = new SettingsCheckbox
+ {
+ LabelText = "a sample component",
+ }
+ });
+ }
+ });
+ }
+
+ private class OverlayColourContainer : Container
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider;
+
+ public OverlayColourContainer(OverlayColourScheme scheme)
+ {
+ colourProvider = new OverlayColourProvider(scheme);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs
index b14684200f..319a768e65 100644
--- a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null)
{
tournament ??= new TournamentGameBase();
- Task.Run(() => host.Run(tournament))
+ Task.Factory.StartNew(() => host.Run(tournament), TaskCreationOptions.LongRunning)
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
WaitForOrAssert(() => tournament.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return tournament;
diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs
index ee48bdd7d9..d2a39e9db7 100644
--- a/osu.Game/Audio/Effects/AudioFilter.cs
+++ b/osu.Game/Audio/Effects/AudioFilter.cs
@@ -4,7 +4,6 @@
using System.Diagnostics;
using ManagedBass.Fx;
using osu.Framework.Audio.Mixing;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Audio.Effects
@@ -21,10 +20,25 @@ namespace osu.Game.Audio.Effects
private readonly BQFParameters filter;
private readonly BQFType type;
+ private bool isAttached;
+
+ private int cutoff;
+
///
- /// The current cutoff of this filter.
+ /// The cutoff frequency of this filter.
///
- public BindableNumber Cutoff { get; }
+ public int Cutoff
+ {
+ get => cutoff;
+ set
+ {
+ if (value == cutoff)
+ return;
+
+ cutoff = value;
+ updateFilter(cutoff);
+ }
+ }
///
/// A Component that implements a BASS FX BiQuad Filter Effect.
@@ -36,102 +50,96 @@ namespace osu.Game.Audio.Effects
this.mixer = mixer;
this.type = type;
- int initialCutoff;
-
- switch (type)
- {
- case BQFType.HighPass:
- initialCutoff = 1;
- break;
-
- case BQFType.LowPass:
- initialCutoff = MAX_LOWPASS_CUTOFF;
- break;
-
- default:
- initialCutoff = 500; // A default that should ensure audio remains audible for other filters.
- break;
- }
-
- Cutoff = new BindableNumber(initialCutoff)
- {
- MinValue = 1,
- MaxValue = MAX_LOWPASS_CUTOFF
- };
-
filter = new BQFParameters
{
lFilter = type,
- fCenter = initialCutoff,
fBandwidth = 0,
- fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
+ // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
+ fQ = 0.7f
};
- // Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic)
- if (type != BQFType.LowPass && type != BQFType.HighPass)
- attachFilter();
-
- Cutoff.ValueChanged += updateFilter;
+ Cutoff = getInitialCutoff(type);
}
- private void attachFilter()
+ private int getInitialCutoff(BQFType type)
{
- Debug.Assert(!mixer.Effects.Contains(filter));
- mixer.Effects.Add(filter);
- }
-
- private void detachFilter()
- {
- Debug.Assert(mixer.Effects.Contains(filter));
- mixer.Effects.Remove(filter);
- }
-
- private void updateFilter(ValueChangedEvent cutoff)
- {
- // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
- if (type == BQFType.LowPass)
+ switch (type)
{
- if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF)
- {
- detachFilter();
- return;
- }
+ case BQFType.HighPass:
+ return 1;
- if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF)
- attachFilter();
+ case BQFType.LowPass:
+ return MAX_LOWPASS_CUTOFF;
+
+ default:
+ return 500; // A default that should ensure audio remains audible for other filters.
+ }
+ }
+
+ private void updateFilter(int newValue)
+ {
+ switch (type)
+ {
+ case BQFType.LowPass:
+ // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
+ if (newValue >= MAX_LOWPASS_CUTOFF)
+ {
+ ensureDetached();
+ return;
+ }
+
+ break;
+
+ // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
+ case BQFType.HighPass:
+ if (newValue <= 1)
+ {
+ ensureDetached();
+ return;
+ }
+
+ break;
}
- // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
- if (type == BQFType.HighPass)
- {
- if (cutoff.NewValue <= 1)
- {
- detachFilter();
- return;
- }
-
- if (cutoff.OldValue <= 1 && cutoff.NewValue > 1)
- attachFilter();
- }
+ ensureAttached();
var filterIndex = mixer.Effects.IndexOf(filter);
+
if (filterIndex < 0) return;
if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
{
- existingFilter.fCenter = cutoff.NewValue;
+ existingFilter.fCenter = newValue;
// required to update effect with new parameters.
mixer.Effects[filterIndex] = existingFilter;
}
}
+ private void ensureAttached()
+ {
+ if (isAttached)
+ return;
+
+ Debug.Assert(!mixer.Effects.Contains(filter));
+ mixer.Effects.Add(filter);
+ isAttached = true;
+ }
+
+ private void ensureDetached()
+ {
+ if (!isAttached)
+ return;
+
+ Debug.Assert(mixer.Effects.Contains(filter));
+ mixer.Effects.Remove(filter);
+ isAttached = false;
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
-
- if (mixer.Effects.Contains(filter))
- detachFilter();
+ ensureDetached();
}
}
}
diff --git a/osu.Game/Audio/Effects/ITransformableFilter.cs b/osu.Game/Audio/Effects/ITransformableFilter.cs
index e4de4cf8ff..fb6a924f68 100644
--- a/osu.Game/Audio/Effects/ITransformableFilter.cs
+++ b/osu.Game/Audio/Effects/ITransformableFilter.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
@@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects
///
/// The filter cutoff.
///
- BindableNumber Cutoff { get; }
+ int Cutoff { get; set; }
}
public static class FilterableAudioComponentExtensions
@@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects
public static TransformSequence CutoffTo(this T component, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
- => component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing);
+ => component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing);
///
/// Smoothly adjusts filter cutoff over time.
@@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects
public static TransformSequence CutoffTo(this TransformSequence sequence, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
- => sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing));
+ => sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing));
}
}
diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs
index f3434c5153..627e54c803 100644
--- a/osu.Game/Beatmaps/BeatmapConverter.cs
+++ b/osu.Game/Beatmaps/BeatmapConverter.cs
@@ -40,7 +40,13 @@ namespace osu.Game.Beatmaps
public IBeatmap Convert(CancellationToken cancellationToken = default)
{
// We always operate on a clone of the original beatmap, to not modify it game-wide
- return ConvertBeatmap(Beatmap.Clone(), cancellationToken);
+ var original = Beatmap.Clone();
+
+ // Shallow clone isn't enough to ensure we don't mutate beatmap info unexpectedly.
+ // Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`.
+ original.BeatmapInfo = original.BeatmapInfo.Clone();
+
+ return ConvertBeatmap(original, cancellationToken);
}
///
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index ac5b5d7a8a..3bcc00f5de 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -178,7 +178,7 @@ namespace osu.Game.Beatmaps
#region Implementation of IHasOnlineID
- public int? OnlineID => OnlineBeatmapID;
+ public int OnlineID => OnlineBeatmapID ?? -1;
#endregion
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 240db22c00..562cbfabf0 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps
}
}
- protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host)
+ protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider api, GameHost host)
{
return new BeatmapModelDownloader(modelManager, api, host);
}
@@ -176,11 +176,6 @@ namespace osu.Game.Beatmaps
}
}
- ///
- /// Fired when the user requests to view the resulting import.
- ///
- public Action>> PresentImport { set => beatmapModelManager.PostImport = value; }
-
///
/// Delete a beatmap difficulty.
///
@@ -338,5 +333,14 @@ namespace osu.Game.Beatmaps
}
#endregion
+
+ #region Implementation of IPostImports
+
+ public Action>> PostImport
+ {
+ set => beatmapModelManager.PostImport = value;
+ }
+
+ #endregion
}
}
diff --git a/osu.Game/Beatmaps/BeatmapModelDownloader.cs b/osu.Game/Beatmaps/BeatmapModelDownloader.cs
index ae482eeafd..30dc95a966 100644
--- a/osu.Game/Beatmaps/BeatmapModelDownloader.cs
+++ b/osu.Game/Beatmaps/BeatmapModelDownloader.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps
protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
- public BeatmapModelDownloader(BeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null)
+ public BeatmapModelDownloader(IBeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null)
: base(beatmapModelManager, api, host)
{
}
diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs
index 9c0fc5ef8a..76019a15ae 100644
--- a/osu.Game/Beatmaps/BeatmapModelManager.cs
+++ b/osu.Game/Beatmaps/BeatmapModelManager.cs
@@ -192,6 +192,13 @@ namespace osu.Game.Beatmaps
{
var setInfo = beatmapInfo.BeatmapSet;
+ // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
+ // This should hopefully be temporary, assuming said clone is eventually removed.
+ beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty);
+
+ // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
+ beatmapContent.BeatmapInfo = beatmapInfo;
+
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
@@ -202,7 +209,6 @@ namespace osu.Game.Beatmaps
using (ContextFactory.GetForWrite())
{
beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == beatmapInfo.ID);
- beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty);
var metadata = beatmapInfo.Metadata ?? setInfo.Metadata;
diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs
index 8b01831b3c..e8c77e792f 100644
--- a/osu.Game/Beatmaps/BeatmapSetInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs
@@ -91,7 +91,7 @@ namespace osu.Game.Beatmaps
#region Implementation of IHasOnlineID
- public int? OnlineID => OnlineBeatmapSetID;
+ public int OnlineID => OnlineBeatmapSetID ?? -1;
#endregion
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index 8203f2e968..4079a0cd5f 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -15,11 +15,9 @@ namespace osu.Game.Beatmaps.ControlPoints
/// The time at which the control point takes effect.
///
[JsonIgnore]
- public double Time => controlPointGroup?.Time ?? 0;
+ public double Time { get; set; }
- private ControlPointGroup controlPointGroup;
-
- public void AttachGroup(ControlPointGroup pointGroup) => controlPointGroup = pointGroup;
+ public void AttachGroup(ControlPointGroup pointGroup) => Time = pointGroup.Time;
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
@@ -46,6 +44,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public virtual void CopyFrom(ControlPoint other)
{
+ Time = other.Time;
}
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index 3ff40fe194..9d738ecbfb 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -33,14 +33,6 @@ namespace osu.Game.Beatmaps.ControlPoints
private readonly SortedList timingPoints = new SortedList(Comparer.Default);
- ///
- /// All difficulty points.
- ///
- [JsonProperty]
- public IReadOnlyList DifficultyPoints => difficultyPoints;
-
- private readonly SortedList difficultyPoints = new SortedList(Comparer.Default);
-
///
/// All effect points.
///
@@ -55,14 +47,6 @@ namespace osu.Game.Beatmaps.ControlPoints
[JsonIgnore]
public IEnumerable AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray();
- ///
- /// Finds the difficulty control point that is active at .
- ///
- /// The time to find the difficulty control point at.
- /// The difficulty control point.
- [NotNull]
- public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
-
///
/// Finds the effect control point that is active at .
///
@@ -100,7 +84,6 @@ namespace osu.Game.Beatmaps.ControlPoints
{
groups.Clear();
timingPoints.Clear();
- difficultyPoints.Clear();
effectPoints.Clear();
}
@@ -277,10 +260,6 @@ namespace osu.Game.Beatmaps.ControlPoints
case EffectControlPoint _:
existing = EffectPointAt(time);
break;
-
- case DifficultyControlPoint _:
- existing = DifficultyPointAt(time);
- break;
}
return newPoint?.IsRedundant(existing) == true;
@@ -298,9 +277,8 @@ namespace osu.Game.Beatmaps.ControlPoints
effectPoints.Add(typed);
break;
- case DifficultyControlPoint typed:
- difficultyPoints.Add(typed);
- break;
+ default:
+ throw new ArgumentException($"A control point of unexpected type {controlPoint.GetType()} was added to this {nameof(ControlPointInfo)}");
}
}
@@ -315,10 +293,6 @@ namespace osu.Game.Beatmaps.ControlPoints
case EffectControlPoint typed:
effectPoints.Remove(typed);
break;
-
- case DifficultyControlPoint typed:
- difficultyPoints.Remove(typed);
- break;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 8a6cfaf688..bf7ed8e6f5 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -7,17 +7,20 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
+ ///
+ /// Note that going forward, this control point type should always be assigned directly to HitObjects.
+ ///
public class DifficultyControlPoint : ControlPoint
{
public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint
{
- SpeedMultiplierBindable = { Disabled = true },
+ SliderVelocityBindable = { Disabled = true },
};
///
- /// The speed multiplier at this control point.
+ /// The slider velocity at this control point.
///
- public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1)
+ public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1)
{
Precision = 0.01,
Default = 1,
@@ -28,21 +31,21 @@ namespace osu.Game.Beatmaps.ControlPoints
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
///
- /// The speed multiplier at this control point.
+ /// The slider velocity at this control point.
///
- public double SpeedMultiplier
+ public double SliderVelocity
{
- get => SpeedMultiplierBindable.Value;
- set => SpeedMultiplierBindable.Value = value;
+ get => SliderVelocityBindable.Value;
+ set => SliderVelocityBindable.Value = value;
}
public override bool IsRedundant(ControlPoint existing)
=> existing is DifficultyControlPoint existingDifficulty
- && SpeedMultiplier == existingDifficulty.SpeedMultiplier;
+ && SliderVelocity == existingDifficulty.SliderVelocity;
public override void CopyFrom(ControlPoint other)
{
- SpeedMultiplier = ((DifficultyControlPoint)other).SpeedMultiplier;
+ SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity;
base.CopyFrom(other);
}
diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
index 79bc88e773..7f550a52fc 100644
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -12,7 +12,8 @@ namespace osu.Game.Beatmaps.ControlPoints
public static readonly EffectControlPoint DEFAULT = new EffectControlPoint
{
KiaiModeBindable = { Disabled = true },
- OmitFirstBarLineBindable = { Disabled = true }
+ OmitFirstBarLineBindable = { Disabled = true },
+ ScrollSpeedBindable = { Disabled = true }
};
///
@@ -20,6 +21,26 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public readonly BindableBool OmitFirstBarLineBindable = new BindableBool();
+ ///
+ /// The relative scroll speed at this control point.
+ ///
+ public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1)
+ {
+ Precision = 0.01,
+ Default = 1,
+ MinValue = 0.01,
+ MaxValue = 10
+ };
+
+ ///
+ /// The relative scroll speed.
+ ///
+ public double ScrollSpeed
+ {
+ get => ScrollSpeedBindable.Value;
+ set => ScrollSpeedBindable.Value = value;
+ }
+
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple;
///
@@ -49,12 +70,14 @@ namespace osu.Game.Beatmaps.ControlPoints
=> !OmitFirstBarLine
&& existing is EffectControlPoint existingEffect
&& KiaiMode == existingEffect.KiaiMode
- && OmitFirstBarLine == existingEffect.OmitFirstBarLine;
+ && OmitFirstBarLine == existingEffect.OmitFirstBarLine
+ && ScrollSpeed == existingEffect.ScrollSpeed;
public override void CopyFrom(ControlPoint other)
{
KiaiMode = ((EffectControlPoint)other).KiaiMode;
OmitFirstBarLine = ((EffectControlPoint)other).OmitFirstBarLine;
+ ScrollSpeed = ((EffectControlPoint)other).ScrollSpeed;
base.CopyFrom(other);
}
diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
index 4aa6a3d6e9..fb489f73b1 100644
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -8,6 +8,9 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
+ ///
+ /// Note that going forward, this control point type should always be assigned directly to HitObjects.
+ ///
public class SampleControlPoint : ControlPoint
{
public const string DEFAULT_BANK = "normal";
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index f71b148008..bef2d78f21 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -384,14 +384,21 @@ namespace osu.Game.Beatmaps.Formats
addControlPoint(time, new LegacyDifficultyControlPoint(beatLength)
#pragma warning restore 618
{
- SpeedMultiplier = speedMultiplier,
+ SliderVelocity = speedMultiplier,
}, timingChange);
- addControlPoint(time, new EffectControlPoint
+ var effectPoint = new EffectControlPoint
{
KiaiMode = kiaiMode,
OmitFirstBarLine = omitFirstBarSignature,
- }, timingChange);
+ };
+
+ bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0;
+ // scrolling rulesets use effect points rather than difficulty points for scroll speed adjustments.
+ if (!isOsuRuleset)
+ effectPoint.ScrollSpeed = speedMultiplier;
+
+ addControlPoint(time, effectPoint, timingChange);
addControlPoint(time, new LegacySampleControlPoint
{
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 74b3c178cd..1dc270ee63 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -170,33 +170,30 @@ namespace osu.Game.Beatmaps.Formats
if (beatmap.ControlPointInfo.Groups.Count == 0)
return;
+ var legacyControlPoints = new LegacyControlPointInfo();
+ foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
+ legacyControlPoints.Add(point.Time, point.DeepClone());
+
writer.WriteLine("[TimingPoints]");
- if (!(beatmap.ControlPointInfo is LegacyControlPointInfo))
+ SampleControlPoint lastRelevantSamplePoint = null;
+ DifficultyControlPoint lastRelevantDifficultyPoint = null;
+
+ bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0;
+
+ // iterate over hitobjects and pull out all required sample and difficulty changes
+ extractDifficultyControlPoints(beatmap.HitObjects);
+ extractSampleControlPoints(beatmap.HitObjects);
+
+ // handle scroll speed, which is stored as "slider velocity" in legacy formats.
+ // this is relevant for scrolling ruleset beatmaps.
+ if (!isOsuRuleset)
{
- var legacyControlPoints = new LegacyControlPointInfo();
-
- foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
- legacyControlPoints.Add(point.Time, point.DeepClone());
-
- beatmap.ControlPointInfo = legacyControlPoints;
-
- SampleControlPoint lastRelevantSamplePoint = null;
-
- // iterate over hitobjects and pull out all required sample changes
- foreach (var h in beatmap.HitObjects)
- {
- var hSamplePoint = h.SampleControlPoint;
-
- if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint))
- {
- legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint);
- lastRelevantSamplePoint = hSamplePoint;
- }
- }
+ foreach (var point in legacyControlPoints.EffectPoints)
+ legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed });
}
- foreach (var group in beatmap.ControlPointInfo.Groups)
+ foreach (var group in legacyControlPoints.Groups)
{
var groupTimingPoint = group.ControlPoints.OfType().FirstOrDefault();
@@ -209,16 +206,16 @@ namespace osu.Game.Beatmaps.Formats
}
// Output any remaining effects as secondary non-timing control point.
- var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time);
+ var difficultyPoint = legacyControlPoints.DifficultyPointAt(group.Time);
writer.Write(FormattableString.Invariant($"{group.Time},"));
- writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SpeedMultiplier},"));
+ writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SliderVelocity},"));
outputControlPointAt(group.Time, false);
}
void outputControlPointAt(double time, bool isTimingPoint)
{
- var samplePoint = ((LegacyControlPointInfo)beatmap.ControlPointInfo).SamplePointAt(time);
- var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time);
+ var samplePoint = legacyControlPoints.SamplePointAt(time);
+ var effectPoint = legacyControlPoints.EffectPointAt(time);
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)
HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty));
@@ -230,7 +227,7 @@ namespace osu.Game.Beatmaps.Formats
if (effectPoint.OmitFirstBarLine)
effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
- writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(time).TimeSignature},"));
+ writer.Write(FormattableString.Invariant($"{(int)legacyControlPoints.TimingPointAt(time).TimeSignature},"));
writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},"));
writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},"));
writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},"));
@@ -238,6 +235,55 @@ namespace osu.Game.Beatmaps.Formats
writer.Write(FormattableString.Invariant($"{(int)effectFlags}"));
writer.WriteLine();
}
+
+ IEnumerable collectDifficultyControlPoints(IEnumerable hitObjects)
+ {
+ if (!isOsuRuleset)
+ yield break;
+
+ foreach (var hitObject in hitObjects)
+ {
+ yield return hitObject.DifficultyControlPoint;
+
+ foreach (var nested in collectDifficultyControlPoints(hitObject.NestedHitObjects))
+ yield return nested;
+ }
+ }
+
+ void extractDifficultyControlPoints(IEnumerable hitObjects)
+ {
+ foreach (var hDifficultyPoint in collectDifficultyControlPoints(hitObjects).OrderBy(dp => dp.Time))
+ {
+ if (!hDifficultyPoint.IsRedundant(lastRelevantDifficultyPoint))
+ {
+ legacyControlPoints.Add(hDifficultyPoint.Time, hDifficultyPoint);
+ lastRelevantDifficultyPoint = hDifficultyPoint;
+ }
+ }
+ }
+
+ IEnumerable collectSampleControlPoints(IEnumerable hitObjects)
+ {
+ foreach (var hitObject in hitObjects)
+ {
+ yield return hitObject.SampleControlPoint;
+
+ foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects))
+ yield return nested;
+ }
+ }
+
+ void extractSampleControlPoints(IEnumerable hitObject)
+ {
+ foreach (var hSamplePoint in collectSampleControlPoints(hitObject).OrderBy(sp => sp.Time))
+ {
+ if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint))
+ {
+ legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint);
+ lastRelevantSamplePoint = hSamplePoint;
+ }
+ }
+ }
}
private void handleColours(TextWriter writer)
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 20080308f9..cf6c827af5 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -181,7 +181,7 @@ namespace osu.Game.Beatmaps.Formats
public LegacyDifficultyControlPoint()
{
- SpeedMultiplierBindable.Precision = double.Epsilon;
+ SliderVelocityBindable.Precision = double.Epsilon;
}
public override void CopyFrom(ControlPoint other)
diff --git a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs
index ff0ca5ebe1..2b0a2e7a4d 100644
--- a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs
+++ b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs
@@ -1,9 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
-using osu.Framework.Bindables;
+using osu.Framework.Lists;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Beatmaps.Legacy
@@ -14,9 +15,9 @@ namespace osu.Game.Beatmaps.Legacy
/// All sound points.
///
[JsonProperty]
- public IBindableList SamplePoints => samplePoints;
+ public IReadOnlyList SamplePoints => samplePoints;
- private readonly BindableList samplePoints = new BindableList();
+ private readonly SortedList samplePoints = new SortedList(Comparer.Default);
///
/// Finds the sound control point that is active at .
@@ -26,35 +27,76 @@ namespace osu.Game.Beatmaps.Legacy
[NotNull]
public SampleControlPoint SamplePointAt(double time) => BinarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
+ ///
+ /// All difficulty points.
+ ///
+ [JsonProperty]
+ public IReadOnlyList DifficultyPoints => difficultyPoints;
+
+ private readonly SortedList difficultyPoints = new SortedList(Comparer.Default);
+
+ ///
+ /// Finds the difficulty control point that is active at .
+ ///
+ /// The time to find the difficulty control point at.
+ /// The difficulty control point.
+ [NotNull]
+ public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
+
public override void Clear()
{
base.Clear();
samplePoints.Clear();
+ difficultyPoints.Clear();
}
protected override bool CheckAlreadyExisting(double time, ControlPoint newPoint)
{
- if (newPoint is SampleControlPoint)
+ switch (newPoint)
{
- var existing = BinarySearch(SamplePoints, time);
- return newPoint.IsRedundant(existing);
- }
+ case SampleControlPoint _:
+ // intentionally don't use SamplePointAt (we always need to consider the first sample point).
+ var existing = BinarySearch(SamplePoints, time);
+ return newPoint.IsRedundant(existing);
- return base.CheckAlreadyExisting(time, newPoint);
+ case DifficultyControlPoint _:
+ return newPoint.IsRedundant(DifficultyPointAt(time));
+
+ default:
+ return base.CheckAlreadyExisting(time, newPoint);
+ }
}
protected override void GroupItemAdded(ControlPoint controlPoint)
{
- if (controlPoint is SampleControlPoint typed)
- samplePoints.Add(typed);
+ switch (controlPoint)
+ {
+ case SampleControlPoint typed:
+ samplePoints.Add(typed);
+ return;
- base.GroupItemAdded(controlPoint);
+ case DifficultyControlPoint typed:
+ difficultyPoints.Add(typed);
+ return;
+
+ default:
+ base.GroupItemAdded(controlPoint);
+ break;
+ }
}
protected override void GroupItemRemoved(ControlPoint controlPoint)
{
- if (controlPoint is SampleControlPoint typed)
- samplePoints.Remove(typed);
+ switch (controlPoint)
+ {
+ case SampleControlPoint typed:
+ samplePoints.Remove(typed);
+ break;
+
+ case DifficultyControlPoint typed:
+ difficultyPoints.Remove(typed);
+ break;
+ }
base.GroupItemRemoved(controlPoint);
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 18adecb7aa..d2c0f7de0f 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -189,11 +189,14 @@ namespace osu.Game.Beatmaps
///
public void CancelAsyncLoad()
{
- loadCancellation?.Cancel();
- loadCancellation = new CancellationTokenSource();
+ lock (beatmapFetchLock)
+ {
+ loadCancellation?.Cancel();
+ loadCancellation = new CancellationTokenSource();
- if (beatmapLoadTask?.IsCompleted != true)
- beatmapLoadTask = null;
+ if (beatmapLoadTask?.IsCompleted != true)
+ beatmapLoadTask = null;
+ }
}
private CancellationTokenSource createCancellationTokenSource(TimeSpan? timeout)
@@ -205,19 +208,27 @@ namespace osu.Game.Beatmaps
return new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10));
}
- private Task loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() =>
+ private readonly object beatmapFetchLock = new object();
+
+ private Task loadBeatmapAsync()
{
- // Todo: Handle cancellation during beatmap parsing
- var b = GetBeatmap() ?? new Beatmap();
+ lock (beatmapFetchLock)
+ {
+ return beatmapLoadTask ??= Task.Factory.StartNew(() =>
+ {
+ // Todo: Handle cancellation during beatmap parsing
+ var b = GetBeatmap() ?? new Beatmap();
- // The original beatmap version needs to be preserved as the database doesn't contain it
- BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion;
+ // The original beatmap version needs to be preserved as the database doesn't contain it
+ BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion;
- // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc)
- b.BeatmapInfo = BeatmapInfo;
+ // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc)
+ b.BeatmapInfo = BeatmapInfo;
- return b;
- }, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ return b;
+ }, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ }
+ }
public override string ToString() => BeatmapInfo.ToString();
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index ad3e890b3a..cf83345e2a 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -66,8 +66,12 @@ namespace osu.Game.Beatmaps
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID);
+
if (working != null)
+ {
+ Logger.Log($"Invalidating working beatmap cache for {info}");
workingCache.Remove(working);
+ }
}
}
@@ -86,6 +90,7 @@ namespace osu.Game.Beatmaps
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
+
if (working != null)
return working;
diff --git a/osu.Game/Configuration/RandomSelectAlgorithm.cs b/osu.Game/Configuration/RandomSelectAlgorithm.cs
index 8d0c87374f..b22f2ae485 100644
--- a/osu.Game/Configuration/RandomSelectAlgorithm.cs
+++ b/osu.Game/Configuration/RandomSelectAlgorithm.cs
@@ -10,7 +10,7 @@ namespace osu.Game.Configuration
[Description("Never repeat")]
RandomPermutation,
- [Description("Random")]
+ [Description("True Random")]
Random
}
}
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index ee1a7e2900..84e33e3f36 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Database
///
/// The model type.
/// The associated file join type.
- public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager, IPostImports
+ public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager
where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete
where TFileModel : class, INamedFileInfo, new()
{
@@ -116,7 +116,7 @@ namespace osu.Game.Database
/// One or more archive locations on disk.
public Task Import(params string[] paths)
{
- var notification = new ProgressNotification { State = ProgressNotificationState.Active };
+ var notification = new ImportProgressNotification();
PostNotification?.Invoke(notification);
@@ -125,7 +125,7 @@ namespace osu.Game.Database
public Task Import(params ImportTask[] tasks)
{
- var notification = new ProgressNotification { State = ProgressNotificationState.Active };
+ var notification = new ImportProgressNotification();
PostNotification?.Invoke(notification);
diff --git a/osu.Game/Database/IHasOnlineID.cs b/osu.Game/Database/IHasOnlineID.cs
index c55c461d2d..529c68a8f8 100644
--- a/osu.Game/Database/IHasOnlineID.cs
+++ b/osu.Game/Database/IHasOnlineID.cs
@@ -8,8 +8,8 @@ namespace osu.Game.Database
public interface IHasOnlineID
{
///
- /// The server-side ID representing this instance, if one exists.
+ /// The server-side ID representing this instance, if one exists. -1 denotes a missing ID.
///
- int? OnlineID { get; }
+ int OnlineID { get; }
}
}
diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs
new file mode 100644
index 0000000000..024d9f2a89
--- /dev/null
+++ b/osu.Game/Database/IHasRealmFiles.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Models;
+
+#nullable enable
+
+namespace osu.Game.Database
+{
+ ///
+ /// A model that contains a list of files it is responsible for.
+ ///
+ public interface IHasRealmFiles
+ {
+ IList Files { get; }
+
+ string Hash { get; set; }
+ }
+}
diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs
index e94af01772..479f33c3b4 100644
--- a/osu.Game/Database/IModelImporter.cs
+++ b/osu.Game/Database/IModelImporter.cs
@@ -10,10 +10,10 @@ using osu.Game.Overlays.Notifications;
namespace osu.Game.Database
{
///
- /// A class which handles importing of asociated models to the game store.
+ /// A class which handles importing of associated models to the game store.
///
/// The model type.
- public interface IModelImporter : IPostNotifications
+ public interface IModelImporter : IPostNotifications, IPostImports
where TModel : class
{
///
diff --git a/osu.Game/Database/INamedFile.cs b/osu.Game/Database/INamedFile.cs
new file mode 100644
index 0000000000..2bd45d4e42
--- /dev/null
+++ b/osu.Game/Database/INamedFile.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Models;
+
+#nullable enable
+
+namespace osu.Game.Database
+{
+ ///
+ /// Represents a join model which gives a filename and scope to a .
+ ///
+ public interface INamedFile
+ {
+ string Filename { get; set; }
+
+ RealmFile File { get; set; }
+ }
+}
diff --git a/osu.Game/Database/ImportProgressNotification.cs b/osu.Game/Database/ImportProgressNotification.cs
new file mode 100644
index 0000000000..aaee3e117f
--- /dev/null
+++ b/osu.Game/Database/ImportProgressNotification.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Overlays.Notifications;
+
+namespace osu.Game.Database
+{
+ public class ImportProgressNotification : ProgressNotification
+ {
+ public ImportProgressNotification()
+ {
+ State = ProgressNotificationState.Active;
+ }
+ }
+}
diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs
index 0ff902a8bc..b5c44927ca 100644
--- a/osu.Game/Database/RealmContextFactory.cs
+++ b/osu.Game/Database/RealmContextFactory.cs
@@ -2,13 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Development;
-using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Statistics;
+using osu.Game.Models;
using Realms;
#nullable enable
@@ -18,7 +19,7 @@ namespace osu.Game.Database
///
/// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage.
///
- public class RealmContextFactory : Component, IRealmFactory
+ public class RealmContextFactory : IDisposable, IRealmFactory
{
private readonly Storage storage;
@@ -27,7 +28,12 @@ namespace osu.Game.Database
///
public readonly string Filename;
- private const int schema_version = 6;
+ ///
+ /// Version history:
+ /// 6 First tracked version (~20211018)
+ /// 7 Changed OnlineID fields to non-nullable to add indexing support (20211018)
+ ///
+ private const int schema_version = 7;
///
/// Lock object which is held during sections, blocking context creation during blocking periods.
@@ -79,10 +85,11 @@ namespace osu.Game.Database
///
public bool Compact() => Realm.Compact(getConfiguration());
- protected override void Update()
+ ///
+ /// Perform a blocking refresh on the main realm context.
+ ///
+ public void Refresh()
{
- base.Update();
-
lock (contextLock)
{
if (context?.Refresh() == true)
@@ -92,7 +99,7 @@ namespace osu.Game.Database
public Realm CreateContext()
{
- if (IsDisposed)
+ if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
try
@@ -120,6 +127,36 @@ namespace osu.Game.Database
private void onMigration(Migration migration, ulong lastSchemaVersion)
{
+ if (lastSchemaVersion < 7)
+ {
+ convertOnlineIDs();
+ convertOnlineIDs();
+ convertOnlineIDs();
+
+ void convertOnlineIDs() where T : RealmObject
+ {
+ var className = typeof(T).Name.Replace(@"Realm", string.Empty);
+
+ // version was not bumped when the beatmap/ruleset models were added
+ // therefore we must manually check for their presence to avoid throwing on the `DynamicApi` calls.
+ if (!migration.OldRealm.Schema.TryFindObjectSchema(className, out _))
+ return;
+
+ var oldItems = migration.OldRealm.DynamicApi.All(className);
+ var newItems = migration.NewRealm.DynamicApi.All(className);
+
+ int itemCount = newItems.Count();
+
+ for (int i = 0; i < itemCount; i++)
+ {
+ var oldItem = oldItems.ElementAt(i);
+ var newItem = newItems.ElementAt(i);
+
+ long? nullableOnlineID = oldItem?.OnlineID;
+ newItem.OnlineID = (int)(nullableOnlineID ?? -1);
+ }
+ }
+ }
}
///
@@ -132,12 +169,11 @@ namespace osu.Game.Database
/// An which should be disposed to end the blocking section.
public IDisposable BlockAllOperations()
{
- if (IsDisposed)
+ if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
- // TODO: this can be added for safety once we figure how to bypass in test
- // if (!ThreadSafety.IsUpdateThread)
- // throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread.");
+ if (!ThreadSafety.IsUpdateThread)
+ throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread.");
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
@@ -177,21 +213,23 @@ namespace osu.Game.Database
});
}
- protected override void Dispose(bool isDisposing)
+ private bool isDisposed;
+
+ public void Dispose()
{
lock (contextLock)
{
context?.Dispose();
}
- if (!IsDisposed)
+ if (!isDisposed)
{
// intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal.
contextCreationLock.Wait();
contextCreationLock.Dispose();
- }
- base.Dispose(isDisposing);
+ isDisposed = true;
+ }
}
}
}
diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs
new file mode 100644
index 0000000000..abb69644d6
--- /dev/null
+++ b/osu.Game/Database/RealmLive.cs
@@ -0,0 +1,111 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Threading;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Database
+{
+ ///
+ /// Provides a method of working with realm objects over longer application lifetimes.
+ ///
+ /// The underlying object type.
+ public class RealmLive : ILive where T : RealmObject, IHasGuidPrimaryKey
+ {
+ public Guid ID { get; }
+
+ private readonly SynchronizationContext? fetchedContext;
+ private readonly int fetchedThreadId;
+
+ ///
+ /// The original live data used to create this instance.
+ ///
+ private readonly T data;
+
+ ///
+ /// Construct a new instance of live realm data.
+ ///
+ /// The realm data.
+ public RealmLive(T data)
+ {
+ this.data = data;
+
+ fetchedContext = SynchronizationContext.Current;
+ fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
+
+ ID = data.ID;
+ }
+
+ ///
+ /// Perform a read operation on this live object.
+ ///
+ /// The action to perform.
+ public void PerformRead(Action perform)
+ {
+ if (originalDataValid)
+ {
+ perform(data);
+ return;
+ }
+
+ using (var realm = Realm.GetInstance(data.Realm.Config))
+ perform(realm.Find(ID));
+ }
+
+ ///
+ /// Perform a read operation on this live object.
+ ///
+ /// The action to perform.
+ public TReturn PerformRead(Func perform)
+ {
+ if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
+ throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
+
+ if (originalDataValid)
+ return perform(data);
+
+ using (var realm = Realm.GetInstance(data.Realm.Config))
+ return perform(realm.Find(ID));
+ }
+
+ ///
+ /// Perform a write operation on this live object.
+ ///
+ /// The action to perform.
+ public void PerformWrite(Action perform) =>
+ PerformRead(t =>
+ {
+ var transaction = t.Realm.BeginWrite();
+ perform(t);
+ transaction.Commit();
+ });
+
+ public T Value
+ {
+ get
+ {
+ if (originalDataValid)
+ return data;
+
+ T retrieved;
+
+ using (var realm = Realm.GetInstance(data.Realm.Config))
+ retrieved = realm.Find(ID);
+
+ if (!retrieved.IsValid)
+ throw new InvalidOperationException("Attempted to access value without an open context");
+
+ return retrieved;
+ }
+ }
+
+ private bool originalDataValid => isCorrectThread && data.IsValid;
+
+ // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72)
+ private bool isCorrectThread
+ => (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId;
+ }
+}
diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs
index c5aa1399a3..18a926fa8c 100644
--- a/osu.Game/Database/RealmObjectExtensions.cs
+++ b/osu.Game/Database/RealmObjectExtensions.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using AutoMapper;
using osu.Game.Input.Bindings;
using Realms;
@@ -47,5 +48,17 @@ namespace osu.Game.Database
return mapper.Map(item);
}
+
+ public static List> ToLive(this IEnumerable realmList)
+ where T : RealmObject, IHasGuidPrimaryKey
+ {
+ return realmList.Select(l => new RealmLive(l)).ToList();
+ }
+
+ public static RealmLive ToLive(this T realmObject)
+ where T : RealmObject, IHasGuidPrimaryKey
+ {
+ return new RealmLive(realmObject);
+ }
}
}
diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs
index aaad72f65c..017ea6ec32 100644
--- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs
@@ -1,11 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@@ -141,12 +144,12 @@ namespace osu.Game.Graphics.Containers
Child = box = new Box { RelativeSizeAxes = Axes.Both };
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
Colour = defaultColour = colours.Gray8;
hoverColour = colours.GrayF;
- highlightColour = colours.Green;
+ highlightColour = colourProvider?.Highlight1 ?? colours.Green;
}
public override void ResizeTo(float val, int duration = 0, Easing easing = Easing.None)
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index d7cfc4094c..af2bb26871 100644
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -225,6 +225,16 @@ namespace osu.Game.Graphics
public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee");
public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff");
+ ///
+ /// Equivalent to 's .
+ ///
+ public readonly Color4 Pink3 = Color4Extensions.FromHex(@"cc3378");
+
+ ///
+ /// Equivalent to 's .
+ ///
+ public readonly Color4 Blue3 = Color4Extensions.FromHex(@"3399cc");
+
///
/// Equivalent to 's .
///
diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs
index 6807d007bb..8f0fed580f 100644
--- a/osu.Game/Graphics/UserInterface/Nub.cs
+++ b/osu.Game/Graphics/UserInterface/Nub.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using JetBrains.Annotations;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
@@ -12,63 +13,74 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
+using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface
{
- public class Nub : CircularContainer, IHasCurrentValue, IHasAccentColour
+ public class Nub : CompositeDrawable, IHasCurrentValue, IHasAccentColour
{
- public const float COLLAPSED_SIZE = 20;
- public const float EXPANDED_SIZE = 40;
+ public const float HEIGHT = 15;
+
+ public const float EXPANDED_SIZE = 50;
private const float border_width = 3;
- private const double animate_in_duration = 150;
+ private const double animate_in_duration = 200;
private const double animate_out_duration = 500;
+ private readonly Box fill;
+ private readonly Container main;
+
public Nub()
{
- Box fill;
+ Size = new Vector2(EXPANDED_SIZE, HEIGHT);
- Size = new Vector2(COLLAPSED_SIZE, 12);
-
- BorderColour = Color4.White;
- BorderThickness = border_width;
-
- Masking = true;
-
- Children = new[]
+ InternalChildren = new[]
{
- fill = new Box
+ main = new CircularContainer
{
+ BorderColour = Color4.White,
+ BorderThickness = border_width,
+ Masking = true,
RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Children = new Drawable[]
+ {
+ fill = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ },
+ }
},
};
-
- Current.ValueChanged += filled =>
- {
- fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint);
- this.TransformTo(nameof(BorderThickness), filled.NewValue ? 8.5f : border_width, 200, Easing.OutQuint);
- };
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour colours)
{
- AccentColour = colours.Pink;
- GlowingAccentColour = colours.PinkLighter;
- GlowColour = colours.PinkDarker;
+ AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
+ GlowingAccentColour = colourProvider?.Highlight1.Lighten(0.2f) ?? colours.PinkLighter;
+ GlowColour = colourProvider?.Highlight1 ?? colours.PinkLighter;
- EdgeEffect = new EdgeEffectParameters
+ main.EdgeEffect = new EdgeEffectParameters
{
Colour = GlowColour.Opacity(0),
Type = EdgeEffectType.Glow,
- Radius = 10,
- Roundness = 8,
+ Radius = 8,
+ Roundness = 5,
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Current.BindValueChanged(onCurrentValueChanged, true);
+ }
+
private bool glowing;
public bool Glowing
@@ -80,28 +92,17 @@ namespace osu.Game.Graphics.UserInterface
if (value)
{
- this.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint);
- FadeEdgeEffectTo(1, animate_in_duration, Easing.OutQuint);
+ main.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint);
+ main.FadeEdgeEffectTo(0.2f, animate_in_duration, Easing.OutQuint);
}
else
{
- FadeEdgeEffectTo(0, animate_out_duration);
- this.FadeColour(AccentColour, animate_out_duration);
+ main.FadeEdgeEffectTo(0, animate_out_duration, Easing.OutQuint);
+ main.FadeColour(AccentColour, animate_out_duration, Easing.OutQuint);
}
}
}
- public bool Expanded
- {
- set
- {
- if (value)
- this.ResizeTo(new Vector2(EXPANDED_SIZE, 12), animate_in_duration, Easing.OutQuint);
- else
- this.ResizeTo(new Vector2(COLLAPSED_SIZE, 12), animate_out_duration, Easing.OutQuint);
- }
- }
-
private readonly Bindable current = new Bindable();
public Bindable Current
@@ -126,7 +127,7 @@ namespace osu.Game.Graphics.UserInterface
{
accentColour = value;
if (!Glowing)
- Colour = value;
+ main.Colour = value;
}
}
@@ -139,7 +140,7 @@ namespace osu.Game.Graphics.UserInterface
{
glowingAccentColour = value;
if (Glowing)
- Colour = value;
+ main.Colour = value;
}
}
@@ -152,10 +153,22 @@ namespace osu.Game.Graphics.UserInterface
{
glowColour = value;
- var effect = EdgeEffect;
+ var effect = main.EdgeEffect;
effect.Colour = Glowing ? value : value.Opacity(0);
- EdgeEffect = effect;
+ main.EdgeEffect = effect;
}
}
+
+ private void onCurrentValueChanged(ValueChangedEvent filled)
+ {
+ fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint);
+
+ if (filled.NewValue)
+ main.ResizeWidthTo(1, animate_in_duration, Easing.OutElasticHalf);
+ else
+ main.ResizeWidthTo(0.9f, animate_out_duration, Easing.OutElastic);
+
+ main.TransformTo(nameof(BorderThickness), filled.NewValue ? 8.5f : border_width, 200, Easing.OutQuint);
+ }
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
index 5f2d884cd7..e8f80dec57 100644
--- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
@@ -9,16 +9,11 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
-using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public class OsuCheckbox : Checkbox
{
- public Color4 CheckedColor { get; set; } = Color4.Cyan;
- public Color4 UncheckedColor { get; set; } = Color4.White;
- public int FadeDuration { get; set; }
-
///
/// Whether to play sounds when the state changes as a result of user interaction.
///
@@ -104,14 +99,12 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
Nub.Glowing = true;
- Nub.Expanded = true;
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
Nub.Glowing = false;
- Nub.Expanded = false;
base.OnHoverLost(e);
}
diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
index fe88e6f78a..5831d9ab1f 100644
--- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
@@ -1,8 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System.Linq;
-using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -14,13 +15,15 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
+using osu.Game.Overlays;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public class OsuDropdown : Dropdown, IHasAccentColour
{
- private const float corner_radius = 4;
+ private const float corner_radius = 5;
private Color4 accentColour;
@@ -34,11 +37,11 @@ namespace osu.Game.Graphics.UserInterface
}
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
if (accentColour == default)
- accentColour = colours.PinkDarker;
+ accentColour = colourProvider?.Light4 ?? colours.PinkDarker;
updateAccentColour();
}
@@ -59,14 +62,13 @@ namespace osu.Game.Graphics.UserInterface
{
public override bool HandleNonPositionalInput => State == MenuState.Open;
- private Sample sampleOpen;
- private Sample sampleClose;
+ private Sample? sampleOpen;
+ private Sample? sampleClose;
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
public OsuDropdownMenu()
{
CornerRadius = corner_radius;
- BackgroundColour = Color4.Black.Opacity(0.5f);
MaskingContainer.CornerRadius = corner_radius;
Alpha = 0;
@@ -75,9 +77,11 @@ namespace osu.Game.Graphics.UserInterface
ItemsContainer.Padding = new MarginPadding(5);
}
- [BackgroundDependencyLoader]
- private void load(AudioManager audio)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, AudioManager audio)
{
+ BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
+
sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
sampleClose = audio.Samples.Get(@"UI/dropdown-close");
}
@@ -159,6 +163,8 @@ namespace osu.Game.Graphics.UserInterface
{
BackgroundColourHover = accentColour ?? nonAccentHoverColour;
BackgroundColourSelected = accentColour ?? nonAccentSelectedColour;
+ BackgroundColour = BackgroundColourHover.Opacity(0);
+
UpdateBackgroundColour();
UpdateForegroundColour();
}
@@ -178,8 +184,6 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- BackgroundColour = Color4.Transparent;
-
nonAccentHoverColour = colours.PinkDarker;
nonAccentSelectedColour = Color4.Black.Opacity(0.5f);
updateColours();
@@ -187,16 +191,29 @@ namespace osu.Game.Graphics.UserInterface
AddInternal(new HoverSounds());
}
+ protected override void UpdateBackgroundColour()
+ {
+ if (!IsPreSelected && !IsSelected)
+ {
+ Background.FadeOut(600, Easing.OutQuint);
+ return;
+ }
+
+ Background.FadeIn(100, Easing.OutQuint);
+ Background.FadeColour(IsPreSelected ? BackgroundColourHover : BackgroundColourSelected, 100, Easing.OutQuint);
+ }
+
protected override void UpdateForegroundColour()
{
base.UpdateForegroundColour();
- if (Foreground.Children.FirstOrDefault() is Content content) content.Chevron.Alpha = IsHovered ? 1 : 0;
+ if (Foreground.Children.FirstOrDefault() is Content content)
+ content.Hovering = IsHovered;
}
protected override Drawable CreateContent() => new Content();
- protected new class Content : FillFlowContainer, IHasText
+ protected new class Content : CompositeDrawable, IHasText
{
public LocalisableString Text
{
@@ -207,32 +224,64 @@ namespace osu.Game.Graphics.UserInterface
public readonly OsuSpriteText Label;
public readonly SpriteIcon Chevron;
+ private const float chevron_offset = -3;
+
public Content()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- Direction = FillDirection.Horizontal;
- Children = new Drawable[]
+ InternalChildren = new Drawable[]
{
Chevron = new SpriteIcon
{
- AlwaysPresent = true,
Icon = FontAwesome.Solid.ChevronRight,
- Colour = Color4.Black,
- Alpha = 0.5f,
Size = new Vector2(8),
+ Alpha = 0,
+ X = chevron_offset,
Margin = new MarginPadding { Left = 3, Right = 3 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
Label = new OsuSpriteText
{
+ X = 15,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
};
}
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider)
+ {
+ Chevron.Colour = colourProvider?.Background5 ?? Color4.Black;
+ }
+
+ private bool hovering;
+
+ public bool Hovering
+ {
+ get => hovering;
+ set
+ {
+ if (value == hovering)
+ return;
+
+ hovering = value;
+
+ if (hovering)
+ {
+ Chevron.FadeIn(400, Easing.OutQuint);
+ Chevron.MoveToX(0, 400, Easing.OutQuint);
+ }
+ else
+ {
+ Chevron.FadeOut(200);
+ Chevron.MoveToX(chevron_offset, 200, Easing.In);
+ }
+ }
+ }
}
}
@@ -267,7 +316,7 @@ namespace osu.Game.Graphics.UserInterface
public OsuDropdownHeader()
{
- Foreground.Padding = new MarginPadding(4);
+ Foreground.Padding = new MarginPadding(10);
AutoSizeAxes = Axes.None;
Margin = new MarginPadding { Bottom = 4 };
@@ -303,8 +352,7 @@ namespace osu.Game.Graphics.UserInterface
Icon = FontAwesome.Solid.ChevronDown,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
- Margin = new MarginPadding { Horizontal = 5 },
- Size = new Vector2(12),
+ Size = new Vector2(16),
},
}
}
@@ -313,11 +361,11 @@ namespace osu.Game.Graphics.UserInterface
AddInternal(new HoverClickSounds());
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
- BackgroundColour = Color4.Black.Opacity(0.5f);
- BackgroundColourHover = colours.PinkDarker;
+ BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
+ BackgroundColourHover = colourProvider?.Light4 ?? colours.PinkDarker;
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index f85f9327fa..6963f7335e 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -3,11 +3,13 @@
using System;
using System.Globalization;
+using JetBrains.Annotations;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
@@ -16,6 +18,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
+using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface
{
@@ -52,34 +55,63 @@ namespace osu.Game.Graphics.UserInterface
{
accentColour = value;
leftBox.Colour = value;
+ }
+ }
+
+ private Colour4 backgroundColour;
+
+ public Color4 BackgroundColour
+ {
+ get => backgroundColour;
+ set
+ {
+ backgroundColour = value;
rightBox.Colour = value;
}
}
public OsuSliderBar()
{
- Height = 12;
- RangePadding = 20;
+ Height = Nub.HEIGHT;
+ RangePadding = Nub.EXPANDED_SIZE / 2;
Children = new Drawable[]
{
- leftBox = new Box
+ new Container
{
- Height = 2,
- EdgeSmoothness = new Vector2(0, 0.5f),
- Position = new Vector2(2, 0),
- RelativeSizeAxes = Axes.None,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
- },
- rightBox = new Box
- {
- Height = 2,
- EdgeSmoothness = new Vector2(0, 0.5f),
- Position = new Vector2(-2, 0),
- RelativeSizeAxes = Axes.None,
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- Alpha = 0.5f,
+ Padding = new MarginPadding { Horizontal = 2 },
+ Child = new CircularContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Masking = true,
+ CornerRadius = 5f,
+ Children = new Drawable[]
+ {
+ leftBox = new Box
+ {
+ Height = 5,
+ EdgeSmoothness = new Vector2(0, 0.5f),
+ RelativeSizeAxes = Axes.None,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ rightBox = new Box
+ {
+ Height = 5,
+ EdgeSmoothness = new Vector2(0, 0.5f),
+ RelativeSizeAxes = Axes.None,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Alpha = 0.5f,
+ },
+ },
+ },
},
nubContainer = new Container
{
@@ -88,7 +120,7 @@ namespace osu.Game.Graphics.UserInterface
{
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
- Expanded = true,
+ Current = { Value = true }
},
},
new HoverClickSounds()
@@ -97,11 +129,12 @@ namespace osu.Game.Graphics.UserInterface
Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; };
}
- [BackgroundDependencyLoader]
- private void load(AudioManager audio, OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load(AudioManager audio, [CanBeNull] OverlayColourProvider colourProvider, OsuColour colours)
{
sample = audio.Samples.Get(@"UI/notch-tick");
- AccentColour = colours.Pink;
+ AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
+ BackgroundColour = colourProvider?.Background5 ?? colours.Pink.Opacity(0.5f);
}
protected override void Update()
@@ -119,26 +152,25 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
- Nub.Glowing = true;
+ updateGlow();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
- Nub.Glowing = false;
+ updateGlow();
base.OnHoverLost(e);
}
- protected override bool OnMouseDown(MouseDownEvent e)
+ protected override void OnDragEnd(DragEndEvent e)
{
- Nub.Current.Value = true;
- return base.OnMouseDown(e);
+ updateGlow();
+ base.OnDragEnd(e);
}
- protected override void OnMouseUp(MouseUpEvent e)
+ private void updateGlow()
{
- Nub.Current.Value = false;
- base.OnMouseUp(e);
+ Nub.Glowing = IsHovered || IsDragged;
}
protected override void OnUserChange(T value)
diff --git a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs
index 965734792c..c01ee1a059 100644
--- a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs
@@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
-using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
@@ -15,30 +12,13 @@ namespace osu.Game.Graphics.UserInterface
{
protected override DropdownHeader CreateHeader() => new SlimDropdownHeader();
- protected override DropdownMenu CreateMenu() => new SlimMenu();
-
private class SlimDropdownHeader : OsuDropdownHeader
{
public SlimDropdownHeader()
{
Height = 25;
- Icon.Size = new Vector2(16);
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 };
}
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
- BackgroundColour = Color4.Black.Opacity(0.25f);
- }
- }
-
- private class SlimMenu : OsuDropdownMenu
- {
- public SlimMenu()
- {
- BackgroundColour = Color4.Black.Opacity(0.7f);
- }
}
}
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
index 5a697623c9..d5f76733cf 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
@@ -1,12 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
+using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
@@ -44,6 +47,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
///
protected readonly T Component;
+ private readonly Box background;
private readonly GridContainer grid;
private readonly OsuTextFlowContainer labelText;
private readonly OsuTextFlowContainer descriptionText;
@@ -62,10 +66,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
InternalChildren = new Drawable[]
{
- new Box
+ background = new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex("1c2125"),
},
new FillFlowContainer
{
@@ -146,9 +149,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
}
- [BackgroundDependencyLoader]
- private void load(OsuColour osuColour)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour osuColour)
{
+ background.Colour = colourProvider?.Background4 ?? Color4Extensions.FromHex(@"1c2125");
descriptionText.Colour = osuColour.Yellow;
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
new file mode 100644
index 0000000000..23ebc6e98d
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+
+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(true)]
+ private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours)
+ {
+ BackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateCornerRadius();
+ }
+
+ private void updateCornerRadius() => Content.CornerRadius = DrawHeight / 2;
+
+ public virtual IEnumerable FilterTerms => new[] { Text.ToString() };
+
+ public bool MatchingFilter
+ {
+ set => this.FadeTo(value ? 1 : 0);
+ }
+
+ public bool FilteringActive { get; set; }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs
index a7fd25b554..deb2e6baf6 100644
--- a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -10,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
+using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@@ -66,11 +69,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
};
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
- enabledColour = colours.BlueDark;
- disabledColour = colours.Gray3;
+ enabledColour = colourProvider?.Highlight1 ?? colours.BlueDark;
+ disabledColour = colourProvider?.Background3 ?? colours.Gray3;
switchContainer.Colour = enabledColour;
fill.Colour = disabledColour;
diff --git a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
new file mode 100644
index 0000000000..f5709b5158
--- /dev/null
+++ b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.IO;
+
+namespace osu.Game.IO.FileAbstraction
+{
+ public class StreamFileAbstraction : TagLib.File.IFileAbstraction
+ {
+ public StreamFileAbstraction(string filename, Stream fileStream)
+ {
+ ReadStream = fileStream;
+ Name = filename;
+ }
+
+ public string Name { get; }
+
+ public Stream ReadStream { get; }
+ public Stream WriteStream => ReadStream;
+
+ public void CloseStream(Stream stream)
+ {
+ if (stream == null)
+ throw new ArgumentNullException(nameof(stream));
+
+ stream.Close();
+ }
+ }
+}
diff --git a/osu.Game/Localisation/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs
index aa6eabd7d1..008781c2e5 100644
--- a/osu.Game/Localisation/AudioSettingsStrings.cs
+++ b/osu.Game/Localisation/AudioSettingsStrings.cs
@@ -24,6 +24,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString VolumeHeader => new TranslatableString(getKey(@"volume_header"), @"Volume");
+ ///
+ /// "Output device"
+ ///
+ public static LocalisableString OutputDevice => new TranslatableString(getKey(@"output_device"), @"Output device");
+
///
/// "Master"
///
diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs
index 6d6381b429..fa92187650 100644
--- a/osu.Game/Localisation/GameplaySettingsStrings.cs
+++ b/osu.Game/Localisation/GameplaySettingsStrings.cs
@@ -14,11 +14,36 @@ namespace osu.Game.Localisation
///
public static LocalisableString GameplaySectionHeader => new TranslatableString(getKey(@"gameplay_section_header"), @"Gameplay");
+ ///
+ /// "Beatmap"
+ ///
+ public static LocalisableString BeatmapHeader => new TranslatableString(getKey(@"beatmap_header"), @"Beatmap");
+
///
/// "General"
///
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
+ ///
+ /// "Audio"
+ ///
+ public static LocalisableString AudioHeader => new TranslatableString(getKey(@"audio"), @"Audio");
+
+ ///
+ /// "HUD"
+ ///
+ public static LocalisableString HUDHeader => new TranslatableString(getKey(@"h_u_d"), @"HUD");
+
+ ///
+ /// "Input"
+ ///
+ public static LocalisableString InputHeader => new TranslatableString(getKey(@"input"), @"Input");
+
+ ///
+ /// "Background"
+ ///
+ public static LocalisableString BackgroundHeader => new TranslatableString(getKey(@"background"), @"Background");
+
///
/// "Background dim"
///
diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs
index 0e384f983f..f85cc0f2ae 100644
--- a/osu.Game/Localisation/GraphicsSettingsStrings.cs
+++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs
@@ -104,6 +104,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString HitLighting => new TranslatableString(getKey(@"hit_lighting"), @"Hit lighting");
+ ///
+ /// "Screenshots"
+ ///
+ public static LocalisableString Screenshots => new TranslatableString(getKey(@"screenshots"), @"Screenshots");
+
///
/// "Screenshot format"
///
diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs
new file mode 100644
index 0000000000..a356c9e20b
--- /dev/null
+++ b/osu.Game/Localisation/RulesetSettingsStrings.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class RulesetSettingsStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.RulesetSettings";
+
+ ///
+ /// "Rulesets"
+ ///
+ public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs
index f22b4d6bf5..8b74b94d59 100644
--- a/osu.Game/Localisation/SkinSettingsStrings.cs
+++ b/osu.Game/Localisation/SkinSettingsStrings.cs
@@ -14,6 +14,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString SkinSectionHeader => new TranslatableString(getKey(@"skin_section_header"), @"Skin");
+ ///
+ /// "Current skin"
+ ///
+ public static LocalisableString CurrentSkin => new TranslatableString(getKey(@"current_skin"), @"Current skin");
+
///
/// "Skin layout editor"
///
diff --git a/osu.Game/Models/RealmBeatmap.cs b/osu.Game/Models/RealmBeatmap.cs
new file mode 100644
index 0000000000..9311425cb7
--- /dev/null
+++ b/osu.Game/Models/RealmBeatmap.cs
@@ -0,0 +1,118 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Rulesets;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ ///
+ /// A single beatmap difficulty.
+ ///
+ [ExcludeFromDynamicCompile]
+ [Serializable]
+ [MapTo("Beatmap")]
+ public class RealmBeatmap : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo
+ {
+ [PrimaryKey]
+ public Guid ID { get; set; } = Guid.NewGuid();
+
+ public string DifficultyName { get; set; } = string.Empty;
+
+ public RealmRuleset Ruleset { get; set; } = null!;
+
+ public RealmBeatmapDifficulty Difficulty { get; set; } = null!;
+
+ public RealmBeatmapMetadata Metadata { get; set; } = null!;
+
+ public RealmBeatmapSet? BeatmapSet { get; set; }
+
+ public BeatmapSetOnlineStatus Status
+ {
+ get => (BeatmapSetOnlineStatus)StatusInt;
+ set => StatusInt = (int)value;
+ }
+
+ [MapTo(nameof(Status))]
+ public int StatusInt { get; set; }
+
+ [Indexed]
+ public int OnlineID { get; set; } = -1;
+
+ public double Length { get; set; }
+
+ public double BPM { get; set; }
+
+ public string Hash { get; set; } = string.Empty;
+
+ public double StarRating { get; set; }
+
+ public string MD5Hash { get; set; } = string.Empty;
+
+ [JsonIgnore]
+ public bool Hidden { get; set; }
+
+ public RealmBeatmap(RealmRuleset ruleset, RealmBeatmapDifficulty difficulty, RealmBeatmapMetadata metadata)
+ {
+ Ruleset = ruleset;
+ Difficulty = difficulty;
+ Metadata = metadata;
+ }
+
+ [UsedImplicitly]
+ private RealmBeatmap()
+ {
+ }
+
+ #region Properties we may not want persisted (but also maybe no harm?)
+
+ public double AudioLeadIn { get; set; }
+
+ public float StackLeniency { get; set; } = 0.7f;
+
+ public bool SpecialStyle { get; set; }
+
+ public bool LetterboxInBreaks { get; set; }
+
+ public bool WidescreenStoryboard { get; set; }
+
+ public bool EpilepsyWarning { get; set; }
+
+ public bool SamplesMatchPlaybackRate { get; set; }
+
+ public double DistanceSpacing { get; set; }
+
+ public int BeatDivisor { get; set; }
+
+ public int GridSize { get; set; }
+
+ public double TimelineZoom { get; set; }
+
+ #endregion
+
+ public bool AudioEquals(RealmBeatmap? other) => other != null
+ && BeatmapSet != null
+ && other.BeatmapSet != null
+ && BeatmapSet.Hash == other.BeatmapSet.Hash
+ && Metadata.AudioFile == other.Metadata.AudioFile;
+
+ public bool BackgroundEquals(RealmBeatmap? other) => other != null
+ && BeatmapSet != null
+ && other.BeatmapSet != null
+ && BeatmapSet.Hash == other.BeatmapSet.Hash
+ && Metadata.BackgroundFile == other.Metadata.BackgroundFile;
+
+ IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
+ IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
+ IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
+ IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => Difficulty;
+ }
+}
diff --git a/osu.Game/Models/RealmBeatmapDifficulty.cs b/osu.Game/Models/RealmBeatmapDifficulty.cs
new file mode 100644
index 0000000000..3c1dad69e4
--- /dev/null
+++ b/osu.Game/Models/RealmBeatmapDifficulty.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [MapTo("BeatmapDifficulty")]
+ public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo
+ {
+ public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
+ public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
+ public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
+ public float ApproachRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
+
+ public double SliderMultiplier { get; set; } = 1;
+ public double SliderTickRate { get; set; } = 1;
+
+ ///
+ /// Returns a shallow-clone of this .
+ ///
+ public RealmBeatmapDifficulty Clone()
+ {
+ var diff = new RealmBeatmapDifficulty();
+ CopyTo(diff);
+ return diff;
+ }
+
+ public void CopyTo(RealmBeatmapDifficulty difficulty)
+ {
+ difficulty.ApproachRate = ApproachRate;
+ difficulty.DrainRate = DrainRate;
+ difficulty.CircleSize = CircleSize;
+ difficulty.OverallDifficulty = OverallDifficulty;
+
+ difficulty.SliderMultiplier = SliderMultiplier;
+ difficulty.SliderTickRate = SliderTickRate;
+ }
+ }
+}
diff --git a/osu.Game/Models/RealmBeatmapMetadata.cs b/osu.Game/Models/RealmBeatmapMetadata.cs
new file mode 100644
index 0000000000..6ea7170d0f
--- /dev/null
+++ b/osu.Game/Models/RealmBeatmapMetadata.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using Newtonsoft.Json;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [Serializable]
+ [MapTo("BeatmapMetadata")]
+ public class RealmBeatmapMetadata : RealmObject, IBeatmapMetadataInfo
+ {
+ public string Title { get; set; } = string.Empty;
+
+ [JsonProperty("title_unicode")]
+ public string TitleUnicode { get; set; } = string.Empty;
+
+ public string Artist { get; set; } = string.Empty;
+
+ [JsonProperty("artist_unicode")]
+ public string ArtistUnicode { get; set; } = string.Empty;
+
+ public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User.
+
+ public string Source { get; set; } = string.Empty;
+
+ [JsonProperty(@"tags")]
+ public string Tags { get; set; } = string.Empty;
+
+ ///
+ /// The time in milliseconds to begin playing the track for preview purposes.
+ /// If -1, the track should begin playing at 40% of its length.
+ ///
+ public int PreviewTime { get; set; }
+
+ public string AudioFile { get; set; } = string.Empty;
+ public string BackgroundFile { get; set; } = string.Empty;
+ }
+}
diff --git a/osu.Game/Models/RealmBeatmapSet.cs b/osu.Game/Models/RealmBeatmapSet.cs
new file mode 100644
index 0000000000..d6e56fd61c
--- /dev/null
+++ b/osu.Game/Models/RealmBeatmapSet.cs
@@ -0,0 +1,79 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [MapTo("BeatmapSet")]
+ public class RealmBeatmapSet : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IBeatmapSetInfo
+ {
+ [PrimaryKey]
+ public Guid ID { get; set; } = Guid.NewGuid();
+
+ [Indexed]
+ public int OnlineID { get; set; } = -1;
+
+ public DateTimeOffset DateAdded { get; set; }
+
+ public IBeatmapMetadataInfo? Metadata => Beatmaps.FirstOrDefault()?.Metadata;
+
+ public IList Beatmaps { get; } = null!;
+
+ public IList Files { get; } = null!;
+
+ public bool DeletePending { get; set; }
+
+ public string Hash { get; set; } = string.Empty;
+
+ ///
+ /// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present).
+ ///
+ public bool Protected { get; set; }
+
+ public double MaxStarDifficulty => Beatmaps.Max(b => b.StarRating);
+
+ public double MaxLength => Beatmaps.Max(b => b.Length);
+
+ public double MaxBPM => Beatmaps.Max(b => b.BPM);
+
+ ///
+ /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
+ /// The path returned is relative to the user file storage.
+ ///
+ /// The name of the file to get the storage path of.
+ public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.StoragePath;
+
+ public override string ToString() => Metadata?.ToString() ?? base.ToString();
+
+ public bool Equals(RealmBeatmapSet? other)
+ {
+ if (other == null)
+ return false;
+
+ if (IsManaged && other.IsManaged)
+ return ID == other.ID;
+
+ if (OnlineID >= 0 && other.OnlineID >= 0)
+ return OnlineID == other.OnlineID;
+
+ if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
+ return Hash == other.Hash;
+
+ return ReferenceEquals(this, other);
+ }
+
+ IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps;
+
+ IEnumerable IBeatmapSetInfo.Files => Files;
+ }
+}
diff --git a/osu.Game/Models/RealmFile.cs b/osu.Game/Models/RealmFile.cs
new file mode 100644
index 0000000000..2715f4be45
--- /dev/null
+++ b/osu.Game/Models/RealmFile.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using osu.Framework.Testing;
+using osu.Game.IO;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [MapTo("File")]
+ public class RealmFile : RealmObject, IFileInfo
+ {
+ [PrimaryKey]
+ public string Hash { get; set; } = string.Empty;
+
+ public string StoragePath => Path.Combine(Hash.Remove(1), Hash.Remove(2), Hash);
+ }
+}
diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs
new file mode 100644
index 0000000000..ba12d51d0b
--- /dev/null
+++ b/osu.Game/Models/RealmNamedFileUsage.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Framework.Testing;
+using osu.Game.Database;
+using osu.Game.IO;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ public class RealmNamedFileUsage : EmbeddedObject, INamedFile, INamedFileUsage
+ {
+ public RealmFile File { get; set; } = null!;
+
+ public string Filename { get; set; } = null!;
+
+ public RealmNamedFileUsage(RealmFile file, string filename)
+ {
+ File = file;
+ Filename = filename;
+ }
+
+ [UsedImplicitly]
+ private RealmNamedFileUsage()
+ {
+ }
+
+ IFileInfo INamedFileUsage.File => File;
+ }
+}
diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Models/RealmRuleset.cs
new file mode 100644
index 0000000000..5d70324713
--- /dev/null
+++ b/osu.Game/Models/RealmRuleset.cs
@@ -0,0 +1,64 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using JetBrains.Annotations;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [MapTo("Ruleset")]
+ public class RealmRuleset : RealmObject, IEquatable, IRulesetInfo
+ {
+ [PrimaryKey]
+ public string ShortName { get; set; } = string.Empty;
+
+ [Indexed]
+ public int OnlineID { get; set; } = -1;
+
+ 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 ?? -1;
+ }
+
+ [UsedImplicitly]
+ private RealmRuleset()
+ {
+ }
+
+ public RealmRuleset(int? onlineID, string name, string shortName, bool available)
+ {
+ OnlineID = onlineID ?? -1;
+ Name = name;
+ ShortName = shortName;
+ Available = available;
+ }
+
+ public bool Available { get; set; }
+
+ public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo;
+
+ public override string ToString() => Name;
+
+ public RealmRuleset Clone() => new RealmRuleset
+ {
+ OnlineID = OnlineID,
+ Name = Name,
+ ShortName = ShortName,
+ InstantiationInfo = InstantiationInfo,
+ Available = Available
+ };
+ }
+}
diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs
index d79fc58d1c..1feb3076d1 100644
--- a/osu.Game/Online/API/OAuth.cs
+++ b/osu.Game/Online/API/OAuth.cs
@@ -39,17 +39,19 @@ namespace osu.Game.Online.API
if (string.IsNullOrEmpty(username)) throw new ArgumentException("Missing username.");
if (string.IsNullOrEmpty(password)) throw new ArgumentException("Missing password.");
- using (var req = new AccessTokenRequestPassword(username, password)
+ var accessTokenRequest = new AccessTokenRequestPassword(username, password)
{
Url = $@"{endpoint}/oauth/token",
Method = HttpMethod.Post,
ClientId = clientId,
ClientSecret = clientSecret
- })
+ };
+
+ using (accessTokenRequest)
{
try
{
- req.Perform();
+ accessTokenRequest.Perform();
}
catch (Exception ex)
{
@@ -60,7 +62,7 @@ namespace osu.Game.Online.API
try
{
// attempt to decode a displayable error string.
- var error = JsonConvert.DeserializeObject(req.GetResponseString() ?? string.Empty);
+ var error = JsonConvert.DeserializeObject(accessTokenRequest.GetResponseString() ?? string.Empty);
if (error != null)
throwableException = new APIException(error.UserDisplayableError, ex);
}
@@ -71,7 +73,7 @@ namespace osu.Game.Online.API
throw throwableException;
}
- Token.Value = req.ResponseObject;
+ Token.Value = accessTokenRequest.ResponseObject;
}
}
@@ -79,17 +81,19 @@ namespace osu.Game.Online.API
{
try
{
- using (var req = new AccessTokenRequestRefresh(refresh)
+ var refreshRequest = new AccessTokenRequestRefresh(refresh)
{
Url = $@"{endpoint}/oauth/token",
Method = HttpMethod.Post,
ClientId = clientId,
ClientSecret = clientSecret
- })
- {
- req.Perform();
+ };
- Token.Value = req.ResponseObject;
+ using (refreshRequest)
+ {
+ refreshRequest.Perform();
+
+ Token.Value = refreshRequest.ResponseObject;
return true;
}
}
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index 0e4ea694aa..201ba6239b 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -177,6 +177,24 @@ namespace osu.Game.Online.Chat
case "wiki":
return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3)));
+
+ case "home":
+ if (mainArg != "changelog")
+ // handle link other than changelog as external for now
+ return new LinkDetails(LinkAction.External, url);
+
+ switch (args.Length)
+ {
+ case 4:
+ // https://osu.ppy.sh/home/changelog
+ return new LinkDetails(LinkAction.OpenChangelog, string.Empty);
+
+ case 6:
+ // https://osu.ppy.sh/home/changelog/lazer/2021.1006
+ return new LinkDetails(LinkAction.OpenChangelog, $"{args[4]}/{args[5]}");
+ }
+
+ break;
}
}
@@ -324,6 +342,7 @@ namespace osu.Game.Online.Chat
SearchBeatmapSet,
OpenWiki,
Custom,
+ OpenChangelog,
}
public class Link : IComparable
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 75bbaec0ef..28505f6b0e 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -374,7 +374,7 @@ namespace osu.Game.Online.Multiplayer
UserJoined?.Invoke(user);
RoomUpdated?.Invoke();
- }, false);
+ });
}
Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) =>
diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs
index 5f71b4be4a..39fc7f1da8 100644
--- a/osu.Game/Online/Rooms/Room.cs
+++ b/osu.Game/Online/Rooms/Room.cs
@@ -130,12 +130,6 @@ namespace osu.Game.Online.Rooms
set => MaxAttempts.Value = value;
}
- ///
- /// The position of this in the list. This is not read from or written to the API.
- ///
- [JsonIgnore]
- public readonly Bindable Position = new Bindable(-1); // Todo: This does not need to exist.
-
public Room()
{
Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue));
@@ -192,8 +186,6 @@ namespace osu.Game.Online.Rooms
RecentParticipants.Clear();
RecentParticipants.AddRange(other.RecentParticipants);
}
-
- Position.Value = other.Position.Value;
}
public void RemoveExpiredPlaylistItems()
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index a3b4d90d20..2cbe05fecd 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -90,6 +90,8 @@ namespace osu.Game
private WikiOverlay wikiOverlay;
+ private ChangelogOverlay changelogOverlay;
+
private SkinEditorOverlay skinEditor;
private Container overlayContent;
@@ -209,13 +211,6 @@ namespace osu.Game
[BackgroundDependencyLoader]
private void load()
{
- if (args?.Length > 0)
- {
- var paths = args.Where(a => !a.StartsWith('-')).ToArray();
- if (paths.Length > 0)
- Task.Run(() => Import(paths));
- }
-
dependencies.CacheAs(this);
dependencies.Cache(SentryLogger);
@@ -336,6 +331,17 @@ namespace osu.Game
ShowWiki(link.Argument);
break;
+ case LinkAction.OpenChangelog:
+ if (string.IsNullOrEmpty(link.Argument))
+ ShowChangelogListing();
+ else
+ {
+ var changelogArgs = link.Argument.Split("/");
+ ShowChangelogBuild(changelogArgs[0], changelogArgs[1]);
+ }
+
+ break;
+
default:
throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
}
@@ -401,6 +407,18 @@ namespace osu.Game
/// The wiki page to show
public void ShowWiki(string path) => waitForReady(() => wikiOverlay, _ => wikiOverlay.ShowPage(path));
+ ///
+ /// Show changelog listing overlay
+ ///
+ public void ShowChangelogListing() => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowListing());
+
+ ///
+ /// Show changelog's build as an overlay
+ ///
+ /// The update stream name
+ /// The build version of the update stream
+ public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
+
///
/// Present a beatmap at song select immediately.
/// The user should have already requested this interactively.
@@ -536,6 +554,7 @@ namespace osu.Game
{
beatmap.OldValue?.CancelAsyncLoad();
beatmap.NewValue?.BeginAsyncLoad();
+ Logger.Log($"Game-wide working beatmap updated to {beatmap.NewValue}");
}
private void modsChanged(ValueChangedEvent> mods)
@@ -624,7 +643,7 @@ namespace osu.Game
SkinManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PostNotification = n => Notifications.Post(n);
- BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value);
+ BeatmapManager.PostImport = items => PresentBeatmap(items.First().Value);
ScoreManager.PostNotification = n => Notifications.Post(n);
ScoreManager.PostImport = items => PresentScore(items.First().Value);
@@ -769,7 +788,7 @@ namespace osu.Game
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);
- var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true);
+ loadComponentSingleFile(changelogOverlay = new ChangelogOverlay(), overlayContent.Add, true);
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true);
@@ -842,6 +861,19 @@ namespace osu.Game
{
if (mode.NewValue != OverlayActivation.All) CloseAllOverlays();
};
+
+ // Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup.
+ handleStartupImport();
+ }
+
+ private void handleStartupImport()
+ {
+ if (args?.Length > 0)
+ {
+ var paths = args.Where(a => !a.StartsWith('-')).ToArray();
+ if (paths.Length > 0)
+ Task.Run(() => Import(paths));
+ }
}
private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 7f4fe8a943..f6ec22a536 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@@ -186,8 +187,6 @@ namespace osu.Game
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client"));
- AddInternal(realmFactory);
-
dependencies.CacheAs(Storage);
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures")));
@@ -410,11 +409,28 @@ namespace osu.Game
{
Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""...");
- using (realmFactory.BlockAllOperations())
+ IDisposable realmBlocker = null;
+
+ try
{
- contextFactory.FlushConnections();
+ ManualResetEventSlim readyToRun = new ManualResetEventSlim();
+
+ Scheduler.Add(() =>
+ {
+ realmBlocker = realmFactory.BlockAllOperations();
+ contextFactory.FlushConnections();
+
+ readyToRun.Set();
+ }, false);
+
+ readyToRun.Wait();
+
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
}
+ finally
+ {
+ realmBlocker?.Dispose();
+ }
Logger.Log(@"Migration complete!");
}
@@ -511,6 +527,7 @@ namespace osu.Game
LocalConfig?.Dispose();
contextFactory?.FlushConnections();
+ realmFactory?.Dispose();
}
}
}
diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs
index 3105ecd742..f8cd31f193 100644
--- a/osu.Game/Overlays/Notifications/ProgressNotification.cs
+++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs
@@ -31,10 +31,12 @@ namespace osu.Game.Overlays.Notifications
set
{
progress = value;
- Scheduler.AddOnce(() => progressBar.Progress = progress);
+ Scheduler.AddOnce(updateProgress, progress);
}
}
+ private void updateProgress(float progress) => progressBar.Progress = progress;
+
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs
index 51214fe460..198aa1438a 100644
--- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs
+++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Overlays.OSD
private Sample sampleChange;
public TrackedSettingToast(SettingDescription description)
- : base(description.Name, description.Value, description.Shortcut)
+ : base(description.Name.ToString(), description.Value.ToString(), description.Shortcut.ToString())
{
FillFlowContainer optionLights;
diff --git a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs
index c02db40eca..4ca3ace8a1 100644
--- a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs
+++ b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs
@@ -14,10 +14,7 @@ namespace osu.Game.Overlays.Settings
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- BackgroundColour = colours.Pink;
-
- Triangles.ColourDark = colours.PinkDark;
- Triangles.ColourLight = colours.PinkLight;
+ BackgroundColour = colours.Pink3;
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
index d697b45424..0c54ae2763 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
@@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
dropdown = new AudioDeviceSettingsDropdown
{
+ LabelText = AudioSettingsStrings.OutputDevice,
Keywords = new[] { "speaker", "headphone", "output" }
}
};
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs
new file mode 100644
index 0000000000..dba64d695a
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections.Gameplay
+{
+ public class AudioSettings : SettingsSubsection
+ {
+ protected override LocalisableString Header => GameplaySettingsStrings.AudioHeader;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ Children = new Drawable[]
+ {
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.PositionalHitsounds,
+ Current = config.GetBindable(OsuSetting.PositionalHitSounds)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak,
+ Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak)
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs
new file mode 100644
index 0000000000..94e0c5e494
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections.Gameplay
+{
+ public class BackgroundSettings : SettingsSubsection
+ {
+ protected override LocalisableString Header => GameplaySettingsStrings.BackgroundHeader;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ Children = new Drawable[]
+ {
+ new SettingsSlider
+ {
+ LabelText = GameplaySettingsStrings.BackgroundDim,
+ Current = config.GetBindable(OsuSetting.DimLevel),
+ KeyboardStep = 0.01f,
+ DisplayAsPercentage = true
+ },
+ new SettingsSlider
+ {
+ LabelText = GameplaySettingsStrings.BackgroundBlur,
+ Current = config.GetBindable(OsuSetting.BlurLevel),
+ KeyboardStep = 0.01f,
+ DisplayAsPercentage = true
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.LightenDuringBreaks,
+ Current = config.GetBindable(OsuSetting.LightenDuringBreaks)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.FadePlayfieldWhenHealthLow,
+ Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow),
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs
new file mode 100644
index 0000000000..aaa60ce81b
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections.Gameplay
+{
+ public class BeatmapSettings : SettingsSubsection
+ {
+ protected override LocalisableString Header => GameplaySettingsStrings.BeatmapHeader;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ Children = new Drawable[]
+ {
+ new SettingsCheckbox
+ {
+ LabelText = SkinSettingsStrings.BeatmapSkins,
+ Current = config.GetBindable(OsuSetting.BeatmapSkins)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = SkinSettingsStrings.BeatmapColours,
+ Current = config.GetBindable(OsuSetting.BeatmapColours)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = SkinSettingsStrings.BeatmapHitsounds,
+ Current = config.GetBindable(OsuSetting.BeatmapHitsounds)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GraphicsSettingsStrings.StoryboardVideo,
+ Current = config.GetBindable(OsuSetting.ShowStoryboard)
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
index 3a0265e453..d4e4fd571d 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
@@ -1,7 +1,6 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
@@ -20,77 +19,18 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
{
Children = new Drawable[]
{
- new SettingsSlider
- {
- LabelText = GameplaySettingsStrings.BackgroundDim,
- Current = config.GetBindable(OsuSetting.DimLevel),
- KeyboardStep = 0.01f,
- DisplayAsPercentage = true
- },
- new SettingsSlider
- {
- LabelText = GameplaySettingsStrings.BackgroundBlur,
- Current = config.GetBindable(OsuSetting.BlurLevel),
- KeyboardStep = 0.01f,
- DisplayAsPercentage = true
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.LightenDuringBreaks,
- Current = config.GetBindable(OsuSetting.LightenDuringBreaks)
- },
- new SettingsEnumDropdown
- {
- LabelText = GameplaySettingsStrings.HUDVisibilityMode,
- Current = config.GetBindable(OsuSetting.HUDVisibilityMode)
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.ShowDifficultyGraph,
- Current = config.GetBindable(OsuSetting.ShowProgressGraph)
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail,
- Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail),
- Keywords = new[] { "hp", "bar" }
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.FadePlayfieldWhenHealthLow,
- Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow),
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay,
- Current = config.GetBindable(OsuSetting.KeyOverlay)
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.PositionalHitsounds,
- Current = config.GetBindable(OsuSetting.PositionalHitSounds)
- },
- new SettingsCheckbox
- {
- LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak,
- Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak)
- },
new SettingsEnumDropdown
{
LabelText = GameplaySettingsStrings.ScoreDisplayMode,
Current = config.GetBindable(OsuSetting.ScoreDisplayMode),
Keywords = new[] { "scoring" }
},
- };
-
- if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
- {
- Add(new SettingsCheckbox
+ new SettingsCheckbox
{
- LabelText = GameplaySettingsStrings.DisableWinKey,
- Current = config.GetBindable(OsuSetting.GameplayDisableWinKey)
- });
- }
+ LabelText = GraphicsSettingsStrings.HitLighting,
+ Current = config.GetBindable(OsuSetting.HitLighting)
+ },
+ };
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs
new file mode 100644
index 0000000000..e1b452e322
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections.Gameplay
+{
+ public class HUDSettings : SettingsSubsection
+ {
+ protected override LocalisableString Header => GameplaySettingsStrings.HUDHeader;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ Children = new Drawable[]
+ {
+ new SettingsEnumDropdown
+ {
+ LabelText = GameplaySettingsStrings.HUDVisibilityMode,
+ Current = config.GetBindable(OsuSetting.HUDVisibilityMode)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.ShowDifficultyGraph,
+ Current = config.GetBindable(OsuSetting.ShowProgressGraph)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail,
+ Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail),
+ Keywords = new[] { "hp", "bar" }
+ },
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay,
+ Current = config.GetBindable(OsuSetting.KeyOverlay)
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs
new file mode 100644
index 0000000000..962572ca6e
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections.Gameplay
+{
+ public class InputSettings : SettingsSubsection
+ {
+ protected override LocalisableString Header => GameplaySettingsStrings.InputHeader;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ Children = new Drawable[]
+ {
+ new SettingsSlider
+ {
+ LabelText = SkinSettingsStrings.GameplayCursorSize,
+ Current = config.GetBindable(OsuSetting.GameplayCursorSize),
+ KeyboardStep = 0.01f
+ },
+ new SettingsCheckbox
+ {
+ LabelText = SkinSettingsStrings.AutoCursorSize,
+ Current = config.GetBindable(OsuSetting.AutoCursorSize)
+ },
+ };
+
+ if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
+ {
+ Add(new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.DisableWinKey,
+ Current = config.GetBindable(OsuSetting.GameplayDisableWinKey)
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
index 42d9d48d73..120e2d908c 100644
--- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
@@ -1,16 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Game.Overlays.Settings.Sections.Gameplay;
-using osu.Game.Rulesets;
-using System.Linq;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Logging;
using osu.Framework.Localisation;
using osu.Game.Localisation;
+using osu.Game.Overlays.Settings.Sections.Gameplay;
namespace osu.Game.Overlays.Settings.Sections
{
@@ -20,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections
public override Drawable CreateIcon() => new SpriteIcon
{
- Icon = FontAwesome.Regular.Circle
+ Icon = FontAwesome.Regular.DotCircle
};
public GameplaySection()
@@ -28,27 +23,13 @@ namespace osu.Game.Overlays.Settings.Sections
Children = new Drawable[]
{
new GeneralSettings(),
+ new AudioSettings(),
+ new BeatmapSettings(),
+ new BackgroundSettings(),
+ new HUDSettings(),
+ new InputSettings(),
new ModsSettings(),
};
}
-
- [BackgroundDependencyLoader]
- private void load(RulesetStore rulesets)
- {
- foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance()))
- {
- try
- {
- SettingsSubsection section = ruleset.CreateSettings();
-
- if (section != null)
- Add(section);
- }
- catch (Exception e)
- {
- Logger.Error(e, "Failed to load ruleset settings");
- }
- }
- }
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs
similarity index 67%
rename from osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs
rename to osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs
index 20b1d8d801..dbb9ddc1c1 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs
@@ -9,25 +9,15 @@ using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Graphics
{
- public class DetailSettings : SettingsSubsection
+ public class ScreenshotSettings : SettingsSubsection
{
- protected override LocalisableString Header => GraphicsSettingsStrings.DetailSettingsHeader;
+ protected override LocalisableString Header => GraphicsSettingsStrings.Screenshots;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
Children = new Drawable[]
{
- new SettingsCheckbox
- {
- LabelText = GraphicsSettingsStrings.StoryboardVideo,
- Current = config.GetBindable(OsuSetting.ShowStoryboard)
- },
- new SettingsCheckbox
- {
- LabelText = GraphicsSettingsStrings.HitLighting,
- Current = config.GetBindable(OsuSetting.HitLighting)
- },
new SettingsEnumDropdown
{
LabelText = GraphicsSettingsStrings.ScreenshotFormat,
diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
index fd0718f9f2..591848506a 100644
--- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
@@ -22,9 +22,9 @@ namespace osu.Game.Overlays.Settings.Sections
{
Children = new Drawable[]
{
- new RendererSettings(),
new LayoutSettings(),
- new DetailSettings(),
+ new RendererSettings(),
+ new ScreenshotSettings(),
};
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
index 39dddbe1e6..2051af6f3c 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
@@ -7,7 +7,6 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Game.Database;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Localisation;
@@ -59,7 +58,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
}
- public class ResetButton : DangerousTriangleButton
+ public class ResetButton : DangerousSettingsButton
{
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs
index 26610628d5..3ef5ce8941 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -8,16 +9,24 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Handlers.Tablet;
using osu.Game.Graphics;
-using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Overlays.Settings.Sections.Input
{
- internal class RotationPresetButtons : FillFlowContainer
+ internal class RotationPresetButtons : CompositeDrawable
{
+ public new MarginPadding Padding
+ {
+ get => base.Padding;
+ set => base.Padding = value;
+ }
+
private readonly ITabletHandler tabletHandler;
private Bindable rotation;
+ private readonly RotationButton[] rotationPresets = new RotationButton[preset_count];
+ private const int preset_count = 4;
private const int height = 50;
public RotationPresetButtons(ITabletHandler tabletHandler)
@@ -27,18 +36,39 @@ namespace osu.Game.Overlays.Settings.Sections.Input
RelativeSizeAxes = Axes.X;
Height = height;
- for (int i = 0; i < 360; i += 90)
+ IEnumerable createColumns(int count)
{
- var presetRotation = i;
-
- Add(new RotationButton(i)
+ for (int i = 0; i < count; ++i)
{
- RelativeSizeAxes = Axes.X,
- Height = height,
- Width = 0.25f,
- Text = $@"{presetRotation}º",
- Action = () => tabletHandler.Rotation.Value = presetRotation,
- });
+ if (i > 0)
+ yield return new Dimension(GridSizeMode.Absolute, 10);
+
+ yield return new Dimension();
+ }
+ }
+
+ GridContainer grid;
+
+ InternalChild = grid = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = createColumns(preset_count).ToArray()
+ };
+
+ grid.Content = new[] { new Drawable[preset_count * 2 - 1] };
+
+ for (int i = 0; i < preset_count; i++)
+ {
+ var rotationValue = i * 90;
+
+ var rotationPreset = new RotationButton(rotationValue)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 1,
+ Text = $@"{rotationValue}º",
+ Action = () => tabletHandler.Rotation.Value = rotationValue,
+ };
+ grid.Content[0][2 * i] = rotationPresets[i] = rotationPreset;
}
}
@@ -49,16 +79,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input
rotation = tabletHandler.Rotation.GetBoundCopy();
rotation.BindValueChanged(val =>
{
- foreach (var b in Children.OfType())
+ foreach (var b in rotationPresets)
b.IsSelected = b.Preset == val.NewValue;
}, true);
}
- public class RotationButton : TriangleButton
+ public class RotationButton : RoundedButton
{
[Resolved]
private OsuColour colours { get; set; }
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
+
public readonly int Preset;
public RotationButton(int preset)
@@ -91,18 +124,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private void updateColour()
{
- if (isSelected)
- {
- BackgroundColour = colours.BlueDark;
- Triangles.ColourDark = colours.BlueDarker;
- Triangles.ColourLight = colours.Blue;
- }
- else
- {
- BackgroundColour = colours.Gray4;
- Triangles.ColourDark = colours.Gray5;
- Triangles.ColourLight = colours.Gray6;
- }
+ BackgroundColour = isSelected ? colours.Blue3 : colourProvider.Background3;
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
index 803c8332c1..43df58a8b1 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
@@ -10,7 +10,6 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Scoring;
using osu.Game.Skinning;
@@ -21,15 +20,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
protected override LocalisableString Header => "General";
- private TriangleButton importBeatmapsButton;
- private TriangleButton importScoresButton;
- private TriangleButton importSkinsButton;
- private TriangleButton importCollectionsButton;
- private TriangleButton deleteBeatmapsButton;
- private TriangleButton deleteScoresButton;
- private TriangleButton deleteSkinsButton;
- private TriangleButton restoreButton;
- private TriangleButton undeleteButton;
+ private SettingsButton importBeatmapsButton;
+ private SettingsButton importScoresButton;
+ private SettingsButton importSkinsButton;
+ private SettingsButton importCollectionsButton;
+ private SettingsButton deleteBeatmapsButton;
+ private SettingsButton deleteScoresButton;
+ private SettingsButton deleteSkinsButton;
+ private SettingsButton restoreButton;
+ private SettingsButton undeleteButton;
[BackgroundDependencyLoader(permitNulls: true)]
private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] StableImportManager stableImportManager, DialogOverlay dialogOverlay)
diff --git a/osu.Game/Overlays/Settings/Sections/RulesetSection.cs b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs
new file mode 100644
index 0000000000..b9339d5299
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Framework.Logging;
+using osu.Game.Rulesets;
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.Settings.Sections
+{
+ public class RulesetSection : SettingsSection
+ {
+ public override LocalisableString Header => RulesetSettingsStrings.Rulesets;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Chess
+ };
+
+ [BackgroundDependencyLoader]
+ private void load(RulesetStore rulesets)
+ {
+ foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance()))
+ {
+ try
+ {
+ SettingsSubsection section = ruleset.CreateSettings();
+
+ if (section != null)
+ Add(section);
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Failed to load ruleset settings");
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index d18099eb0a..00198235c5 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -64,39 +64,16 @@ namespace osu.Game.Overlays.Settings.Sections
{
Children = new Drawable[]
{
- skinDropdown = new SkinSettingsDropdown(),
+ skinDropdown = new SkinSettingsDropdown
+ {
+ LabelText = SkinSettingsStrings.CurrentSkin
+ },
new SettingsButton
{
Text = SkinSettingsStrings.SkinLayoutEditor,
Action = () => skinEditor?.Toggle(),
},
new ExportSkinButton(),
- new SettingsSlider
- {
- LabelText = SkinSettingsStrings.GameplayCursorSize,
- Current = config.GetBindable(OsuSetting.GameplayCursorSize),
- KeyboardStep = 0.01f
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.AutoCursorSize,
- Current = config.GetBindable(OsuSetting.AutoCursorSize)
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.BeatmapSkins,
- Current = config.GetBindable(OsuSetting.BeatmapSkins)
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.BeatmapColours,
- Current = config.GetBindable(OsuSetting.BeatmapColours)
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.BeatmapHitsounds,
- Current = config.GetBindable(OsuSetting.BeatmapHitsounds)
- },
};
managerUpdated = skins.ItemUpdated.GetBoundCopy();
diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs
index 87b1aa0e46..be7f2de480 100644
--- a/osu.Game/Overlays/Settings/SettingsButton.cs
+++ b/osu.Game/Overlays/Settings/SettingsButton.cs
@@ -6,11 +6,11 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
-using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Overlays.Settings
{
- public class SettingsButton : TriangleButton, IHasTooltip
+ public class SettingsButton : RoundedButton, IHasTooltip
{
public SettingsButton()
{
diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs
index 1175ddaab8..a281d03ee7 100644
--- a/osu.Game/Overlays/Settings/SettingsDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs
@@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
+using osuTK;
namespace osu.Game.Overlays.Settings
{
@@ -27,6 +28,11 @@ namespace osu.Game.Overlays.Settings
public override IEnumerable FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString()));
+ public SettingsDropdown()
+ {
+ FlowContent.Spacing = new Vector2(0, 10);
+ }
+
protected sealed override Drawable CreateControl() => CreateDropdown();
protected virtual OsuDropdown CreateDropdown() => new DropdownControl();
@@ -35,7 +41,6 @@ namespace osu.Game.Overlays.Settings
{
public DropdownControl()
{
- Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X;
}
diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
index 9987a0c607..199ba14b48 100644
--- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
@@ -16,7 +16,6 @@ namespace osu.Game.Overlays.Settings
{
public DropdownControl()
{
- Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X;
}
diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs
index 9fc3379b94..bb9c0dd4d7 100644
--- a/osu.Game/Overlays/Settings/SettingsSlider.cs
+++ b/osu.Game/Overlays/Settings/SettingsSlider.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings
{
protected override Drawable CreateControl() => new TSlider
{
- Margin = new MarginPadding { Top = 5, Bottom = 5 },
+ Margin = new MarginPadding { Vertical = 10 },
RelativeSizeAxes = Axes.X
};
diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs
index 55e8aee266..c84cba8189 100644
--- a/osu.Game/Overlays/SettingsOverlay.cs
+++ b/osu.Game/Overlays/SettingsOverlay.cs
@@ -24,12 +24,13 @@ namespace osu.Game.Overlays
protected override IEnumerable CreateSections() => new SettingsSection[]
{
new GeneralSection(),
- new GraphicsSection(),
- new AudioSection(),
+ new SkinSection(),
new InputSection(createSubPanel(new KeyBindingPanel())),
new UserInterfaceSection(),
new GameplaySection(),
- new SkinSection(),
+ new RulesetSection(),
+ new AudioSection(),
+ new GraphicsSection(),
new OnlineSection(),
new MaintenanceSection(),
new DebugSection(),
diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
index a0ec8e3e0e..eec71a3623 100644
--- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
+++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
@@ -47,9 +47,34 @@ namespace osu.Game.Rulesets.Configuration
}
}
+ private readonly HashSet pendingWrites = new HashSet();
+
protected override bool PerformSave()
{
- // do nothing, realm saves immediately
+ TLookup[] changed;
+
+ lock (pendingWrites)
+ {
+ changed = pendingWrites.ToArray();
+ pendingWrites.Clear();
+ }
+
+ if (realmFactory == null)
+ return true;
+
+ using (var context = realmFactory.CreateContext())
+ {
+ context.Write(realm =>
+ {
+ foreach (var c in changed)
+ {
+ var setting = realm.All().First(s => s.RulesetID == rulesetId && s.Variant == variant && s.Key == c.ToString());
+
+ setting.Value = ConfigStore[c].ToString();
+ }
+ });
+ }
+
return true;
}
@@ -80,7 +105,8 @@ namespace osu.Game.Rulesets.Configuration
bindable.ValueChanged += b =>
{
- realmFactory?.Context.Write(() => setting.Value = b.NewValue.ToString());
+ lock (pendingWrites)
+ pendingWrites.Add(lookup);
};
}
}
diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
index 81f4808789..6ed91e983a 100644
--- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
+++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
@@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Edit
new CheckAudioQuality(),
new CheckMutedObjects(),
new CheckFewHitsounds(),
+ new CheckTooShortAudioFiles(),
+ new CheckAudioInVideo(),
+
+ // Files
+ new CheckZeroByteFiles(),
// Compose
new CheckUnsnappedObjects(),
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs
new file mode 100644
index 0000000000..ac2542beb0
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs
@@ -0,0 +1,112 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using osu.Game.IO.FileAbstraction;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Storyboards;
+using TagLib;
+using File = TagLib.File;
+
+namespace osu.Game.Rulesets.Edit.Checks
+{
+ public class CheckAudioInVideo : ICheck
+ {
+ public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Audio track in video files");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateHasAudioTrack(this),
+ new IssueTemplateMissingFile(this),
+ new IssueTemplateFileError(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
+ var videoPaths = new List();
+
+ foreach (var layer in context.WorkingBeatmap.Storyboard.Layers)
+ {
+ foreach (var element in layer.Elements)
+ {
+ if (!(element is StoryboardVideo video))
+ continue;
+
+ // Ensures we don't check the same video file multiple times in case of multiple elements using it.
+ if (!videoPaths.Contains(video.Path))
+ videoPaths.Add(video.Path);
+ }
+ }
+
+ foreach (var filename in videoPaths)
+ {
+ string storagePath = beatmapSet.GetPathForFile(filename);
+
+ if (storagePath == null)
+ {
+ // There's an element in the storyboard that requires this resource, so it being missing is worth warning about.
+ yield return new IssueTemplateMissingFile(this).Create(filename);
+
+ continue;
+ }
+
+ Issue issue;
+
+ try
+ {
+ // We use TagLib here for platform invariance; BASS cannot detect audio presence on Linux.
+ using (Stream data = context.WorkingBeatmap.GetStream(storagePath))
+ using (File tagFile = File.Create(new StreamFileAbstraction(filename, data)))
+ {
+ if (tagFile.Properties.AudioChannels == 0)
+ continue;
+ }
+
+ issue = new IssueTemplateHasAudioTrack(this).Create(filename);
+ }
+ catch (CorruptFileException)
+ {
+ issue = new IssueTemplateFileError(this).Create(filename, "Corrupt file");
+ }
+ catch (UnsupportedFormatException)
+ {
+ issue = new IssueTemplateFileError(this).Create(filename, "Unsupported format");
+ }
+
+ yield return issue;
+ }
+ }
+
+ public class IssueTemplateHasAudioTrack : IssueTemplate
+ {
+ public IssueTemplateHasAudioTrack(ICheck check)
+ : base(check, IssueType.Problem, "\"{0}\" has an audio track.")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename);
+ }
+
+ public class IssueTemplateFileError : IssueTemplate
+ {
+ public IssueTemplateFileError(ICheck check)
+ : base(check, IssueType.Error, "Could not check whether \"{0}\" has an audio track ({1}).")
+ {
+ }
+
+ public Issue Create(string filename, string errorReason) => new Issue(this, filename, errorReason);
+ }
+
+ public class IssueTemplateMissingFile : IssueTemplate
+ {
+ public IssueTemplateMissingFile(ICheck check)
+ : base(check, IssueType.Warning, "Could not check whether \"{0}\" has an audio track, because it is missing.")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs
new file mode 100644
index 0000000000..57f7c60916
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using ManagedBass;
+using osu.Framework.Audio.Callbacks;
+using osu.Game.Rulesets.Edit.Checks.Components;
+
+namespace osu.Game.Rulesets.Edit.Checks
+{
+ public class CheckTooShortAudioFiles : ICheck
+ {
+ private const int ms_threshold = 25;
+ private const int min_bytes_threshold = 100;
+
+ private readonly string[] audioExtensions = { "mp3", "ogg", "wav" };
+
+ public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateTooShort(this),
+ new IssueTemplateBadFormat(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
+
+ foreach (var file in beatmapSet.Files)
+ {
+ using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.StoragePath))
+ {
+ if (data == null)
+ continue;
+
+ var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data));
+ int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle);
+
+ if (decodeStream == 0)
+ {
+ // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it.
+ // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check.
+ if (hasAudioExtension(file.Filename) && probablyHasAudioData(data))
+ yield return new IssueTemplateBadFormat(this).Create(file.Filename);
+
+ continue;
+ }
+
+ long length = Bass.ChannelGetLength(decodeStream);
+ double ms = Bass.ChannelBytes2Seconds(decodeStream, length) * 1000;
+
+ // Extremely short audio files do not play on some soundcards, resulting in nothing being heard in-game for some users.
+ if (ms > 0 && ms < ms_threshold)
+ yield return new IssueTemplateTooShort(this).Create(file.Filename, ms);
+ }
+ }
+ }
+
+ private bool hasAudioExtension(string filename) => audioExtensions.Any(filename.ToLower().EndsWith);
+ private bool probablyHasAudioData(Stream data) => data.Length > min_bytes_threshold;
+
+ public class IssueTemplateTooShort : IssueTemplate
+ {
+ public IssueTemplateTooShort(ICheck check)
+ : base(check, IssueType.Problem, "\"{0}\" is too short ({1:0} ms), should be at least {2:0} ms.")
+ {
+ }
+
+ public Issue Create(string filename, double ms) => new Issue(this, filename, ms, ms_threshold);
+ }
+
+ public class IssueTemplateBadFormat : IssueTemplate
+ {
+ public IssueTemplateBadFormat(ICheck check)
+ : base(check, IssueType.Error, "Could not check whether \"{0}\" is too short (code \"{1}\").")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename, Bass.LastError);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs
new file mode 100644
index 0000000000..3a994fabfa
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using osu.Game.Rulesets.Edit.Checks.Components;
+
+namespace osu.Game.Rulesets.Edit.Checks
+{
+ public class CheckZeroByteFiles : ICheck
+ {
+ public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Files, "Zero-byte files");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateZeroBytes(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
+
+ foreach (var file in beatmapSet.Files)
+ {
+ using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.StoragePath))
+ {
+ if (data?.Length == 0)
+ yield return new IssueTemplateZeroBytes(this).Create(file.Filename);
+ }
+ }
+ }
+
+ public class IssueTemplateZeroBytes : IssueTemplate
+ {
+ public IssueTemplateZeroBytes(ICheck check)
+ : base(check, IssueType.Problem, "\"{0}\" is a 0-byte file.")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index b41e0442bc..91cc80e930 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -13,7 +13,6 @@ using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@@ -389,41 +388,42 @@ namespace osu.Game.Rulesets.Edit
return new SnapResult(screenSpacePosition, targetTime, playfield);
}
- public override float GetBeatSnapDistanceAt(double referenceTime)
+ public override float GetBeatSnapDistanceAt(HitObject referenceObject)
{
- DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime);
- return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / BeatSnapProvider.BeatDivisor);
+ return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * referenceObject.DifficultyControlPoint.SliderVelocity / BeatSnapProvider.BeatDivisor);
}
- public override float DurationToDistance(double referenceTime, double duration)
+ public override float DurationToDistance(HitObject referenceObject, double duration)
{
- double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
- return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime));
+ double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
+ return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject));
}
- public override double DistanceToDuration(double referenceTime, float distance)
+ public override double DistanceToDuration(HitObject referenceObject, float distance)
{
- double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
- return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength;
+ double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
+ return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength;
}
- public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
- => BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime;
+ public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
+ => BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime;
- public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
+ public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
{
- double actualDuration = referenceTime + DistanceToDuration(referenceTime, distance);
+ double startTime = referenceObject.StartTime;
- double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, referenceTime);
+ double actualDuration = startTime + DistanceToDuration(referenceObject, distance);
- double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
+ double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, startTime);
+
+ double beatLength = BeatSnapProvider.GetBeatLengthAtTime(startTime);
// we don't want to exceed the actual duration and snap to a point in the future.
// as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it.
if (snappedEndTime > actualDuration + 1)
snappedEndTime -= beatLength;
- return DurationToDistance(referenceTime, snappedEndTime - referenceTime);
+ return DurationToDistance(referenceObject, snappedEndTime - startTime);
}
#endregion
@@ -466,15 +466,15 @@ namespace osu.Game.Rulesets.Edit
public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null);
- public abstract float GetBeatSnapDistanceAt(double referenceTime);
+ public abstract float GetBeatSnapDistanceAt(HitObject referenceObject);
- public abstract float DurationToDistance(double referenceTime, double duration);
+ public abstract float DurationToDistance(HitObject referenceObject, double duration);
- public abstract double DistanceToDuration(double referenceTime, float distance);
+ public abstract double DistanceToDuration(HitObject referenceObject, float distance);
- public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance);
+ public abstract double GetSnappedDurationFromDistance(HitObject referenceObject, float distance);
- public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance);
+ public abstract float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance);
#endregion
}
diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
index 4664f3808c..743a2f41fc 100644
--- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
+++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Edit
@@ -27,41 +28,41 @@ namespace osu.Game.Rulesets.Edit
///
/// Retrieves the distance between two points within a timing point that are one beat length apart.
///
- /// The time of the timing point.
+ /// An object to be used as a reference point for this operation.
/// The distance between two points residing in the timing point that are one beat length apart.
- float GetBeatSnapDistanceAt(double referenceTime);
+ float GetBeatSnapDistanceAt(HitObject referenceObject);
///
/// Converts a duration to a distance.
///
- /// The time of the timing point which resides in.
+ /// An object to be used as a reference point for this operation.
/// The duration to convert.
/// A value that represents as a distance in the timing point.
- float DurationToDistance(double referenceTime, double duration);
+ float DurationToDistance(HitObject referenceObject, double duration);
///
/// Converts a distance to a duration.
///
- /// The time of the timing point which resides in.
+ /// An object to be used as a reference point for this operation.
/// The distance to convert.
/// A value that represents as a duration in the timing point.
- double DistanceToDuration(double referenceTime, float distance);
+ double DistanceToDuration(HitObject referenceObject, float distance);
///
/// Converts a distance to a snapped duration.
///
- /// The time of the timing point which resides in.
+ /// An object to be used as a reference point for this operation.
/// The distance to convert.
/// A value that represents as a duration snapped to the closest beat of the timing point.
- double GetSnappedDurationFromDistance(double referenceTime, float distance);
+ double GetSnappedDurationFromDistance(HitObject referenceObject, float distance);
///
/// Converts an unsnapped distance to a snapped distance.
/// The returned distance will always be floored (as to never exceed the provided .
///
- /// The time of the timing point which resides in.
+ /// An object to be used as a reference point for this operation.
/// The distance to convert.
/// A value that represents snapped to the closest beat of the timing point.
- float GetSnappedDistanceFromDistance(double referenceTime, float distance);
+ float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance);
}
}
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index 0b159819d4..035ebe10cb 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -67,7 +67,8 @@ namespace osu.Game.Rulesets.Objects
}
}
- public SampleControlPoint SampleControlPoint;
+ public SampleControlPoint SampleControlPoint = SampleControlPoint.DEFAULT;
+ public DifficultyControlPoint DifficultyControlPoint = DifficultyControlPoint.DEFAULT;
///
/// Whether this is in Kiai time.
@@ -94,6 +95,12 @@ namespace osu.Game.Rulesets.Objects
foreach (var nested in nestedHitObjects)
nested.StartTime += offset;
+
+ if (DifficultyControlPoint != DifficultyControlPoint.DEFAULT)
+ DifficultyControlPoint.Time = time.NewValue;
+
+ if (SampleControlPoint != SampleControlPoint.DEFAULT)
+ SampleControlPoint.Time = this.GetEndTime() + control_point_leniency;
};
}
@@ -105,16 +112,21 @@ namespace osu.Game.Rulesets.Objects
/// The cancellation token.
public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default)
{
+ var legacyInfo = controlPointInfo as LegacyControlPointInfo;
+
+ if (legacyInfo != null)
+ {
+ DifficultyControlPoint = (DifficultyControlPoint)legacyInfo.DifficultyPointAt(StartTime).DeepClone();
+ DifficultyControlPoint.Time = StartTime;
+ }
+
ApplyDefaultsToSelf(controlPointInfo, difficulty);
- if (controlPointInfo is LegacyControlPointInfo legacyInfo)
+ // This is done here after ApplyDefaultsToSelf as we may require custom defaults to be applied to have an accurate end time.
+ if (legacyInfo != null)
{
- // This is done here since ApplyDefaultsToSelf may be used to determine the end time
- SampleControlPoint = legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency);
- }
- else
- {
- SampleControlPoint ??= SampleControlPoint.DEFAULT;
+ SampleControlPoint = (SampleControlPoint)legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency).DeepClone();
+ SampleControlPoint.Time = this.GetEndTime() + control_point_leniency;
}
nestedHitObjects.Clear();
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
index e1de82ade7..ad191f7ff5 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
@@ -43,9 +43,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
}
diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs
index ca6a083a58..8cd3fa8c63 100644
--- a/osu.Game/Rulesets/RulesetInfo.cs
+++ b/osu.Game/Rulesets/RulesetInfo.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets
#region Implementation of IHasOnlineID
- public int? OnlineID => ID;
+ public int OnlineID => ID ?? -1;
#endregion
}
diff --git a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
index dcd2cc8b55..23325bcd13 100644
--- a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
+++ b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
@@ -7,7 +7,7 @@ using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Timing
{
///
- /// A control point which adds an aggregated multiplier based on the provided 's BeatLength and 's SpeedMultiplier.
+ /// A control point which adds an aggregated multiplier based on the provided 's BeatLength and 's SpeedMultiplier.
///
public class MultiplierControlPoint : IComparable
{
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Timing
///
/// The aggregate multiplier which this provides.
///
- public double Multiplier => Velocity * DifficultyPoint.SpeedMultiplier * BaseBeatLength / TimingPoint.BeatLength;
+ public double Multiplier => Velocity * EffectPoint.ScrollSpeed * BaseBeatLength / TimingPoint.BeatLength;
///
/// The base beat length to scale the provided multiplier relative to.
@@ -38,9 +38,9 @@ namespace osu.Game.Rulesets.Timing
public TimingControlPoint TimingPoint = new TimingControlPoint();
///
- /// The that provides additional difficulty information for this .
+ /// The that provides additional difficulty information for this .
///
- public DifficultyControlPoint DifficultyPoint = new DifficultyControlPoint();
+ public EffectControlPoint EffectPoint = new EffectControlPoint();
///
/// Creates a . This is required for JSON serialization
diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index e9865f6c8b..c0b339a231 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -55,7 +55,10 @@ namespace osu.Game.Rulesets.UI
///
/// The current direction of playback to be exposed to frame stable children.
///
- private int direction;
+ ///
+ /// Initially it is presumed that playback will proceed in the forward direction.
+ ///
+ private int direction = 1;
[BackgroundDependencyLoader(true)]
private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler)
@@ -139,7 +142,9 @@ namespace osu.Game.Rulesets.UI
state = PlaybackState.NotValid;
}
- if (state == PlaybackState.Valid)
+ // if the proposed time is the same as the current time, assume that the clock will continue progressing in the same direction as previously.
+ // this avoids spurious flips in direction from -1 to 1 during rewinds.
+ if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime)
direction = proposedTime >= manualClock.CurrentTime ? 1 : -1;
double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime);
diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
index 041c5ebef5..2a9d3d1cf0 100644
--- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
@@ -140,25 +140,32 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Merge sequences of timing and difficulty control points to create the aggregate "multiplier" control point
var lastTimingPoint = new TimingControlPoint();
- var lastDifficultyPoint = new DifficultyControlPoint();
+ var lastEffectPoint = new EffectControlPoint();
var allPoints = new SortedList(Comparer.Default);
+
allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints);
- allPoints.AddRange(Beatmap.ControlPointInfo.DifficultyPoints);
+ allPoints.AddRange(Beatmap.ControlPointInfo.EffectPoints);
// Generate the timing points, making non-timing changes use the previous timing change and vice-versa
var timingChanges = allPoints.Select(c =>
{
- if (c is TimingControlPoint timingPoint)
- lastTimingPoint = timingPoint;
- else if (c is DifficultyControlPoint difficultyPoint)
- lastDifficultyPoint = difficultyPoint;
+ switch (c)
+ {
+ case TimingControlPoint timingPoint:
+ lastTimingPoint = timingPoint;
+ break;
+
+ case EffectControlPoint difficultyPoint:
+ lastEffectPoint = difficultyPoint;
+ break;
+ }
return new MultiplierControlPoint(c.Time)
{
Velocity = Beatmap.Difficulty.SliderMultiplier,
BaseBeatLength = baseBeatLength,
TimingPoint = lastTimingPoint,
- DifficultyPoint = lastDifficultyPoint
+ EffectPoint = lastEffectPoint
};
});
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index cf22a8fda4..8494cdcd22 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -25,7 +25,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring
{
- public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles, IPostImports
+ public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles
{
private readonly Scheduler scheduler;
private readonly Func difficulties;
diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
index 730f482f83..6b32ff96c4 100644
--- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
@@ -5,14 +5,15 @@ using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
+using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public abstract class CircularDistanceSnapGrid : DistanceSnapGrid
{
- protected CircularDistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null)
- : base(startPosition, startTime, endTime)
+ protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
+ : base(referenceObject, startPosition, startTime, endTime)
{
}
@@ -79,7 +80,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 normalisedDirection = direction * new Vector2(1f / distance);
Vector2 snappedPosition = StartPosition + normalisedDirection * radialCount * radius;
- return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(StartTime, (snappedPosition - StartPosition).Length));
+ return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(ReferenceObject, (snappedPosition - StartPosition).Length));
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 59f88ac641..9d43e3258a 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
@@ -54,15 +55,20 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
private readonly double? endTime;
+ protected readonly HitObject ReferenceObject;
+
///
/// Creates a new .
///
+ /// A reference object to gather relevant difficulty values from.
/// The position at which the grid should start. The first tick is located one distance spacing length away from this point.
/// The snapping time at .
/// The time at which the snapping grid should end. If null, the grid will continue until the bounds of the screen are exceeded.
- protected DistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null)
+ protected DistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
{
+ ReferenceObject = referenceObject;
this.endTime = endTime;
+
StartPosition = startPosition;
StartTime = startTime;
@@ -80,7 +86,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updateSpacing()
{
- DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime);
+ DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject);
if (endTime == null)
MaxIntervals = int.MaxValue;
@@ -88,7 +94,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
// +1 is added since a snapped hitobject may have its start time slightly less than the snapped time due to floating point errors
double maxDuration = endTime.Value - StartTime + 1;
- MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(StartTime, DistanceSpacing));
+ MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(ReferenceObject, DistanceSpacing));
}
gridCache.Invalidate();
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
index 3248936765..21457ea273 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
@@ -1,27 +1,106 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit.Timing;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
- public class DifficultyPointPiece : TopPointPiece
+ public class DifficultyPointPiece : HitObjectPointPiece, IHasPopover
{
+ private readonly HitObject hitObject;
+
private readonly BindableNumber speedMultiplier;
- public DifficultyPointPiece(DifficultyControlPoint point)
- : base(point)
+ public DifficultyPointPiece(HitObject hitObject)
+ : base(hitObject.DifficultyControlPoint)
{
- speedMultiplier = point.SpeedMultiplierBindable.GetBoundCopy();
+ this.hitObject = hitObject;
- Y = Height;
+ speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy();
}
protected override void LoadComplete()
{
base.LoadComplete();
+
speedMultiplier.BindValueChanged(multiplier => Label.Text = $"{multiplier.NewValue:n2}x", true);
}
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ this.ShowPopover();
+ return true;
+ }
+
+ public Popover GetPopover() => new DifficultyEditPopover(hitObject);
+
+ public class DifficultyEditPopover : OsuPopover
+ {
+ private readonly HitObject hitObject;
+ private readonly DifficultyControlPoint point;
+
+ private SliderWithTextBoxInput sliderVelocitySlider;
+
+ [Resolved(canBeNull: true)]
+ private EditorBeatmap beatmap { get; set; }
+
+ public DifficultyEditPopover(HitObject hitObject)
+ {
+ this.hitObject = hitObject;
+ point = hitObject.DifficultyControlPoint;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ Width = 200,
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ sliderVelocitySlider = new SliderWithTextBoxInput("Velocity")
+ {
+ Current = new DifficultyControlPoint().SliderVelocityBindable,
+ KeyboardStep = 0.1f
+ },
+ new OsuTextFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Text = "Hold shift while dragging the end of an object to adjust velocity while snapping."
+ }
+ }
+ }
+ };
+
+ var selectedPointBindable = point.SliderVelocityBindable;
+
+ // there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint).
+ // generally that level of precision could only be set by externally editing the .osu file, so at the point
+ // a user is looking to update this within the editor it should be safe to obliterate this additional precision.
+ double expectedPrecision = new DifficultyControlPoint().SliderVelocityBindable.Precision;
+ if (selectedPointBindable.Precision < expectedPrecision)
+ selectedPointBindable.Precision = expectedPrecision;
+
+ sliderVelocitySlider.Current = selectedPointBindable;
+ sliderVelocitySlider.Current.BindValueChanged(_ => beatmap?.Update(hitObject));
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs
new file mode 100644
index 0000000000..6b62459c97
--- /dev/null
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit.Compose.Components.Timeline
+{
+ public class HitObjectPointPiece : CircularContainer
+ {
+ private readonly ControlPoint point;
+
+ protected OsuSpriteText Label { get; private set; }
+
+ protected HitObjectPointPiece(ControlPoint point)
+ {
+ this.point = point;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ AutoSizeAxes = Axes.Both;
+
+ Color4 colour = point.GetRepresentingColour(colours);
+
+ InternalChildren = new Drawable[]
+ {
+ new Container
+ {
+ AutoSizeAxes = Axes.X,
+ Height = 16,
+ Masking = true,
+ CornerRadius = 8,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colour,
+ RelativeSizeAxes = Axes.Both,
+ },
+ Label = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Padding = new MarginPadding(5),
+ Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
+ Colour = colours.B5,
+ }
+ }
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
index 9461f5e885..6a26f69e41 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
@@ -3,88 +3,102 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osuTK.Graphics;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit.Timing;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
- public class SamplePointPiece : CompositeDrawable
+ public class SamplePointPiece : HitObjectPointPiece, IHasPopover
{
- private readonly SampleControlPoint samplePoint;
+ private readonly HitObject hitObject;
private readonly Bindable bank;
private readonly BindableNumber volume;
- private OsuSpriteText text;
- private Container volumeBox;
-
- private const int max_volume_height = 22;
-
- public SamplePointPiece(SampleControlPoint samplePoint)
+ public SamplePointPiece(HitObject hitObject)
+ : base(hitObject.SampleControlPoint)
{
- this.samplePoint = samplePoint;
- volume = samplePoint.SampleVolumeBindable.GetBoundCopy();
- bank = samplePoint.SampleBankBindable.GetBoundCopy();
+ this.hitObject = hitObject;
+ volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy();
+ bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- Margin = new MarginPadding { Vertical = 5 };
+ volume.BindValueChanged(volume => updateText());
+ bank.BindValueChanged(bank => updateText(), true);
+ }
- Origin = Anchor.BottomCentre;
- Anchor = Anchor.BottomCentre;
+ protected override bool OnClick(ClickEvent e)
+ {
+ this.ShowPopover();
+ return true;
+ }
- AutoSizeAxes = Axes.X;
- RelativeSizeAxes = Axes.Y;
+ private void updateText()
+ {
+ Label.Text = $"{bank.Value} {volume.Value}";
+ }
- Color4 colour = samplePoint.GetRepresentingColour(colours);
+ public Popover GetPopover() => new SampleEditPopover(hitObject);
- InternalChildren = new Drawable[]
+ public class SampleEditPopover : OsuPopover
+ {
+ private readonly HitObject hitObject;
+ private readonly SampleControlPoint point;
+
+ private LabelledTextBox bank;
+ private SliderWithTextBoxInput volume;
+
+ [Resolved(canBeNull: true)]
+ private EditorBeatmap beatmap { get; set; }
+
+ public SampleEditPopover(HitObject hitObject)
{
- volumeBox = new Circle
+ this.hitObject = hitObject;
+ point = hitObject.SampleControlPoint;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
{
- CornerRadius = 5,
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Y = -20,
- Width = 10,
- Colour = colour,
- },
- new Container
- {
- AutoSizeAxes = Axes.X,
- Height = 16,
- Masking = true,
- CornerRadius = 8,
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Children = new Drawable[]
+ new FillFlowContainer
{
- new Box
+ Width = 200,
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
{
- Colour = colour,
- RelativeSizeAxes = Axes.Both,
- },
- text = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Padding = new MarginPadding(5),
- Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
- Colour = colours.B5,
+ bank = new LabelledTextBox
+ {
+ Label = "Bank Name",
+ },
+ volume = new SliderWithTextBoxInput("Volume")
+ {
+ Current = new SampleControlPoint().SampleVolumeBindable,
+ }
}
}
- },
- };
+ };
- volume.BindValueChanged(volume => volumeBox.Height = max_volume_height * volume.NewValue / 100f, true);
- bank.BindValueChanged(bank => text.Text = bank.NewValue, true);
+ bank.Current = point.SampleBankBindable;
+ bank.Current.BindValueChanged(_ => beatmap.Update(hitObject));
+
+ volume.Current = point.SampleVolumeBindable;
+ volume.Current.BindValueChanged(_ => beatmap.Update(hitObject));
+ }
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 621a24c67d..b8fa05e7eb 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -15,6 +15,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
@@ -58,7 +59,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Track track;
private const float timeline_height = 72;
- private const float timeline_expanded_height = 156;
+ private const float timeline_expanded_height = 94;
public Timeline(Drawable userContent)
{
@@ -158,7 +159,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (visible.NewValue)
{
this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint);
- mainContent.MoveToY(36, 200, Easing.OutQuint);
+ mainContent.MoveToY(20, 200, Easing.OutQuint);
// delay the fade in else masking looks weird.
controlPoints.Delay(180).FadeIn(400, Easing.OutQuint);
@@ -298,14 +299,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private double getTimeFromPosition(Vector2 localPosition) =>
(localPosition.X / Content.DrawWidth) * track.Length;
- public float GetBeatSnapDistanceAt(double referenceTime) => throw new NotImplementedException();
+ public float GetBeatSnapDistanceAt(HitObject referenceObject) => throw new NotImplementedException();
- public float DurationToDistance(double referenceTime, double duration) => throw new NotImplementedException();
+ public float DurationToDistance(HitObject referenceObject, double duration) => throw new NotImplementedException();
- public double DistanceToDuration(double referenceTime, float distance) => throw new NotImplementedException();
+ public double DistanceToDuration(HitObject referenceObject, float distance) => throw new NotImplementedException();
- public double GetSnappedDurationFromDistance(double referenceTime, float distance) => throw new NotImplementedException();
+ public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => throw new NotImplementedException();
- public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => throw new NotImplementedException();
+ public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => throw new NotImplementedException();
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
index c4beb40f92..2b2e66fb18 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
@@ -45,17 +45,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
switch (point)
{
- case DifficultyControlPoint difficultyPoint:
- AddInternal(new DifficultyPointPiece(difficultyPoint) { Depth = -2 });
- break;
-
case TimingControlPoint timingPoint:
AddInternal(new TimingPointPiece(timingPoint));
break;
-
- case SampleControlPoint samplePoint:
- AddInternal(new SamplePointPiece(samplePoint) { Depth = -1 });
- break;
}
}
}, true);
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index 911c9fea51..e2458d45c9 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -13,7 +13,9 @@ using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Threading;
using osu.Framework.Utils;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Edit;
@@ -179,6 +181,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
colouredComponents.Colour = OsuColour.ForegroundTextColourFor(col);
}
+ private SamplePointPiece sampleOverrideDisplay;
+ private DifficultyPointPiece difficultyOverrideDisplay;
+
+ [Resolved]
+ private EditorBeatmap beatmap { get; set; }
+
+ private DifficultyControlPoint difficultyControlPoint;
+ private SampleControlPoint sampleControlPoint;
+
protected override void Update()
{
base.Update();
@@ -194,6 +205,36 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (Item is IHasRepeats repeats)
updateRepeats(repeats);
}
+
+ if (difficultyControlPoint != Item.DifficultyControlPoint)
+ {
+ difficultyControlPoint = Item.DifficultyControlPoint;
+ difficultyOverrideDisplay?.Expire();
+
+ if (Item.DifficultyControlPoint != null && Item is IHasDistance)
+ {
+ AddInternal(difficultyOverrideDisplay = new DifficultyPointPiece(Item)
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.BottomCentre
+ });
+ }
+ }
+
+ if (sampleControlPoint != Item.SampleControlPoint)
+ {
+ sampleControlPoint = Item.SampleControlPoint;
+ sampleOverrideDisplay?.Expire();
+
+ if (Item.SampleControlPoint != null)
+ {
+ AddInternal(sampleOverrideDisplay = new SamplePointPiece(Item)
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.TopCentre
+ });
+ }
+ }
}
private void updateRepeats(IHasRepeats repeats)
@@ -331,39 +372,66 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return true;
}
+ private ScheduledDelegate dragOperation;
+
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
- OnDragHandled?.Invoke(e);
-
- if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time)
+ // schedule is temporary to ensure we don't process multiple times on a single update frame. we need to find a better method of doing this.
+ // without it, a hitobject's endtime may not always be in a valid state (ie. sliders, which needs to recompute their path).
+ dragOperation?.Cancel();
+ dragOperation = Scheduler.Add(() =>
{
- switch (hitObject)
+ OnDragHandled?.Invoke(e);
+
+ if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time)
{
- case IHasRepeats repeatHitObject:
- // find the number of repeats which can fit in the requested time.
- var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1);
- var proposedCount = Math.Max(0, (int)Math.Round((time - hitObject.StartTime) / lengthOfOneRepeat) - 1);
+ switch (hitObject)
+ {
+ case IHasRepeats repeatHitObject:
+ double proposedDuration = time - hitObject.StartTime;
- if (proposedCount == repeatHitObject.RepeatCount)
- return;
+ if (e.CurrentState.Keyboard.ShiftPressed)
+ {
+ if (hitObject.DifficultyControlPoint == DifficultyControlPoint.DEFAULT)
+ hitObject.DifficultyControlPoint = new DifficultyControlPoint();
- repeatHitObject.RepeatCount = proposedCount;
- beatmap.Update(hitObject);
- break;
+ var newVelocity = hitObject.DifficultyControlPoint.SliderVelocity * (repeatHitObject.Duration / proposedDuration);
- case IHasDuration endTimeHitObject:
- var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time));
+ if (Precision.AlmostEquals(newVelocity, hitObject.DifficultyControlPoint.SliderVelocity))
+ return;
- if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, beatmap.GetBeatLengthAtTime(snappedTime)))
- return;
+ hitObject.DifficultyControlPoint.SliderVelocity = newVelocity;
+ beatmap.Update(hitObject);
+ }
+ else
+ {
+ // find the number of repeats which can fit in the requested time.
+ var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1);
+ var proposedCount = Math.Max(0, (int)Math.Round(proposedDuration / lengthOfOneRepeat) - 1);
- endTimeHitObject.Duration = snappedTime - hitObject.StartTime;
- beatmap.Update(hitObject);
- break;
+ if (proposedCount == repeatHitObject.RepeatCount)
+ return;
+
+ repeatHitObject.RepeatCount = proposedCount;
+ beatmap.Update(hitObject);
+ }
+
+ break;
+
+ case IHasDuration endTimeHitObject:
+ var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time));
+
+ if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, beatmap.GetBeatLengthAtTime(snappedTime)))
+ return;
+
+ endTimeHitObject.Duration = snappedTime - hitObject.StartTime;
+ beatmap.Update(hitObject);
+ break;
+ }
}
- }
+ });
}
protected override void OnDragEnd(DragEndEvent e)
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 1170658abb..512226413b 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -162,7 +162,7 @@ namespace osu.Game.Screens.Edit
// todo: remove caching of this and consume via editorBeatmap?
dependencies.Cache(beatDivisor);
- AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin()));
+ AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin(), loadableBeatmap.BeatmapInfo));
dependencies.CacheAs(editorBeatmap);
changeHandler = new EditorChangeHandler(editorBeatmap);
dependencies.CacheAs(changeHandler);
@@ -333,10 +333,10 @@ namespace osu.Game.Screens.Edit
isNewBeatmap = false;
// apply any set-level metadata changes.
- beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet);
+ beatmapManager.Update(editorBeatmap.BeatmapInfo.BeatmapSet);
// save the loaded beatmap's data stream.
- beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin);
+ beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin);
updateLastSavedHash();
}
@@ -523,7 +523,10 @@ namespace osu.Game.Screens.Edit
var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo);
if (!(refetchedBeatmap is DummyWorkingBeatmap))
+ {
+ Logger.Log("Editor providing re-fetched beatmap post edit session");
Beatmap.Value = refetchedBeatmap;
+ }
return base.OnExiting(next);
}
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index 64eb6225fa..2e84ef437a 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@@ -44,6 +45,7 @@ namespace osu.Game.Screens.Edit
///
public readonly Bindable PlacementObject = new Bindable();
+ private readonly BeatmapInfo beatmapInfo;
public readonly IBeatmap PlayableBeatmap;
///
@@ -66,9 +68,37 @@ namespace osu.Game.Screens.Edit
private readonly Dictionary> startTimeBindables = new Dictionary>();
- public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null)
+ public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null, BeatmapInfo beatmapInfo = null)
{
PlayableBeatmap = playableBeatmap;
+
+ // ensure we are not working with legacy control points.
+ // if we leave the legacy points around they will be applied over any local changes on
+ // ApplyDefaults calls. this should eventually be removed once the default logic is moved to the decoder/converter.
+ if (PlayableBeatmap.ControlPointInfo is LegacyControlPointInfo)
+ {
+ var newControlPoints = new ControlPointInfo();
+
+ foreach (var controlPoint in PlayableBeatmap.ControlPointInfo.AllControlPoints)
+ {
+ switch (controlPoint)
+ {
+ case DifficultyControlPoint _:
+ case SampleControlPoint _:
+ // skip legacy types.
+ continue;
+
+ default:
+ newControlPoints.Add(controlPoint.Time, controlPoint);
+ break;
+ }
+ }
+
+ playableBeatmap.ControlPointInfo = newControlPoints;
+ }
+
+ this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo;
+
if (beatmapSkin is Skin skin)
BeatmapSkin = new EditorBeatmapSkin(skin);
@@ -80,11 +110,11 @@ namespace osu.Game.Screens.Edit
public BeatmapInfo BeatmapInfo
{
- get => PlayableBeatmap.BeatmapInfo;
- set => PlayableBeatmap.BeatmapInfo = value;
+ get => beatmapInfo;
+ set => throw new InvalidOperationException();
}
- public BeatmapMetadata Metadata => PlayableBeatmap.Metadata;
+ public BeatmapMetadata Metadata => beatmapInfo.Metadata;
public BeatmapDifficulty Difficulty
{
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index ba83261731..86e5729196 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -25,7 +25,9 @@ namespace osu.Game.Screens.Edit
public double TrackLength => track.Value?.Length ?? 60000;
- public ControlPointInfo ControlPointInfo;
+ public ControlPointInfo ControlPointInfo => Beatmap.ControlPointInfo;
+
+ public IBeatmap Beatmap { get; set; }
private readonly BindableBeatDivisor beatDivisor;
@@ -42,25 +44,15 @@ namespace osu.Game.Screens.Edit
///
public bool IsSeeking { get; private set; }
- public EditorClock(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
- : this(beatmap.ControlPointInfo, beatDivisor)
+ public EditorClock(IBeatmap beatmap = null, BindableBeatDivisor beatDivisor = null)
{
- }
+ Beatmap = beatmap ?? new Beatmap();
- public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor)
- {
- this.beatDivisor = beatDivisor;
-
- ControlPointInfo = controlPointInfo;
+ this.beatDivisor = beatDivisor ?? new BindableBeatDivisor();
underlyingClock = new DecoupleableInterpolatingFramedClock();
}
- public EditorClock()
- : this(new ControlPointInfo(), new BindableBeatDivisor())
- {
- }
-
///
/// Seek to the closest snappable beat from a time.
///
diff --git a/osu.Game/Screens/Edit/EditorRoundedScreen.cs b/osu.Game/Screens/Edit/EditorRoundedScreen.cs
index b271a145f5..508663224d 100644
--- a/osu.Game/Screens/Edit/EditorRoundedScreen.cs
+++ b/osu.Game/Screens/Edit/EditorRoundedScreen.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit
{
new Box
{
- Colour = ColourProvider.Dark4,
+ Colour = ColourProvider.Background3,
RelativeSizeAxes = Axes.Both,
},
roundedContent = new Container
diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
index 9e93b0b038..5bb40c09a5 100644
--- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs
+++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
@@ -10,7 +10,7 @@ using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Setup
{
- internal class MetadataSection : SetupSection
+ public class MetadataSection : SetupSection
{
protected LabelledTextBox ArtistTextBox;
protected LabelledTextBox RomanisedArtistTextBox;
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
index 48639789af..938c7f9cf0 100644
--- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
+++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
@@ -12,8 +12,6 @@ namespace osu.Game.Screens.Edit.Timing
{
new GroupSection(),
new TimingSection(),
- new DifficultySection(),
- new SampleSection(),
new EffectSection(),
};
}
diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
deleted file mode 100644
index 97d110c502..0000000000
--- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Game.Beatmaps.ControlPoints;
-
-namespace osu.Game.Screens.Edit.Timing
-{
- internal class DifficultySection : Section
- {
- private SliderWithTextBoxInput multiplierSlider;
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Flow.AddRange(new[]
- {
- multiplierSlider = new SliderWithTextBoxInput("Speed Multiplier")
- {
- Current = new DifficultyControlPoint().SpeedMultiplierBindable,
- KeyboardStep = 0.1f
- }
- });
- }
-
- protected override void OnControlPointChanged(ValueChangedEvent point)
- {
- if (point.NewValue != null)
- {
- var selectedPointBindable = point.NewValue.SpeedMultiplierBindable;
-
- // there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint).
- // generally that level of precision could only be set by externally editing the .osu file, so at the point
- // a user is looking to update this within the editor it should be safe to obliterate this additional precision.
- double expectedPrecision = new DifficultyControlPoint().SpeedMultiplierBindable.Precision;
- if (selectedPointBindable.Precision < expectedPrecision)
- selectedPointBindable.Precision = expectedPrecision;
-
- multiplierSlider.Current = selectedPointBindable;
- multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
- }
- }
-
- protected override DifficultyControlPoint CreatePoint()
- {
- var reference = Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time);
-
- return new DifficultyControlPoint
- {
- SpeedMultiplier = reference.SpeedMultiplier,
- };
- }
- }
-}
diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs
index 6d23b52c05..c8944d0357 100644
--- a/osu.Game/Screens/Edit/Timing/EffectSection.cs
+++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
@@ -13,13 +14,20 @@ namespace osu.Game.Screens.Edit.Timing
private LabelledSwitchButton kiai;
private LabelledSwitchButton omitBarLine;
+ private SliderWithTextBoxInput scrollSpeedSlider;
+
[BackgroundDependencyLoader]
private void load()
{
- Flow.AddRange(new[]
+ Flow.AddRange(new Drawable[]
{
kiai = new LabelledSwitchButton { Label = "Kiai Time" },
omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" },
+ scrollSpeedSlider = new SliderWithTextBoxInput("Scroll Speed")
+ {
+ Current = new EffectControlPoint().ScrollSpeedBindable,
+ KeyboardStep = 0.1f
+ }
});
}
@@ -32,6 +40,9 @@ namespace osu.Game.Screens.Edit.Timing
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
+
+ scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
+ scrollSpeedSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
}
}
@@ -42,7 +53,8 @@ namespace osu.Game.Screens.Edit.Timing
return new EffectControlPoint
{
KiaiMode = reference.KiaiMode,
- OmitFirstBarLine = reference.OmitFirstBarLine
+ OmitFirstBarLine = reference.OmitFirstBarLine,
+ ScrollSpeed = reference.ScrollSpeed,
};
}
}
diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs
index 7b553ac7ad..a8de476d67 100644
--- a/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs
+++ b/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
public DifficultyRowAttribute(DifficultyControlPoint difficulty)
: base(difficulty, "difficulty")
{
- speedMultiplier = difficulty.SpeedMultiplierBindable.GetBoundCopy();
+ speedMultiplier = difficulty.SliderVelocityBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
@@ -32,7 +32,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
},
text = new AttributeText(Point)
{
- Width = 40,
+ Width = 45,
},
});
diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs
index 812407d6da..1b33fd62aa 100644
--- a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs
+++ b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs
@@ -12,14 +12,18 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
{
private readonly Bindable kiaiMode;
private readonly Bindable omitBarLine;
+ private readonly BindableNumber scrollSpeed;
+
private AttributeText kiaiModeBubble;
private AttributeText omitBarLineBubble;
+ private AttributeText text;
public EffectRowAttribute(EffectControlPoint effect)
: base(effect, "effect")
{
kiaiMode = effect.KiaiModeBindable.GetBoundCopy();
omitBarLine = effect.OmitFirstBarLineBindable.GetBoundCopy();
+ scrollSpeed = effect.ScrollSpeedBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
@@ -27,12 +31,20 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
{
Content.AddRange(new Drawable[]
{
+ new AttributeProgressBar(Point)
+ {
+ Current = scrollSpeed,
+ },
+ text = new AttributeText(Point) { Width = 45 },
kiaiModeBubble = new AttributeText(Point) { Text = "kiai" },
omitBarLineBubble = new AttributeText(Point) { Text = "no barline" },
});
kiaiMode.BindValueChanged(enabled => kiaiModeBubble.FadeTo(enabled.NewValue ? 1 : 0), true);
omitBarLine.BindValueChanged(enabled => omitBarLineBubble.FadeTo(enabled.NewValue ? 1 : 0), true);
+ scrollSpeed.BindValueChanged(_ => updateText(), true);
}
+
+ private void updateText() => text.Text = $"{scrollSpeed.Value:n2}x";
}
}
diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs
deleted file mode 100644
index 52709a2bbe..0000000000
--- a/osu.Game/Screens/Edit/Timing/SampleSection.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics.UserInterfaceV2;
-
-namespace osu.Game.Screens.Edit.Timing
-{
- internal class SampleSection : Section
- {
- private LabelledTextBox bank;
- private SliderWithTextBoxInput volume;
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Flow.AddRange(new Drawable[]
- {
- bank = new LabelledTextBox
- {
- Label = "Bank Name",
- },
- volume = new SliderWithTextBoxInput("Volume")
- {
- Current = new SampleControlPoint().SampleVolumeBindable,
- }
- });
- }
-
- protected override void OnControlPointChanged(ValueChangedEvent point)
- {
- if (point.NewValue != null)
- {
- bank.Current = point.NewValue.SampleBankBindable;
- bank.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
-
- volume.Current = point.NewValue.SampleVolumeBindable;
- volume.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
- }
- }
-
- protected override SampleControlPoint CreatePoint() => new SampleControlPoint(); // TODO: remove
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
index 381849189d..abda9e897b 100644
--- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
@@ -116,8 +116,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
if (ignoredRooms.Contains(room.RoomID.Value.Value))
return;
- room.Position.Value = -room.RoomID.Value.Value;
-
try
{
foreach (var pi in room.Playlist)
@@ -152,6 +150,11 @@ namespace osu.Game.Screens.OnlinePlay.Components
notifyRoomsUpdated();
}
- private void notifyRoomsUpdated() => Scheduler.AddOnce(() => RoomsUpdated?.Invoke());
+ private void notifyRoomsUpdated()
+ {
+ Scheduler.AddOnce(invokeRoomsUpdated);
+
+ void invokeRoomsUpdated() => RoomsUpdated?.Invoke();
+ }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
index 907b7e308a..85efdcef1a 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
@@ -129,7 +129,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private void updateSorting()
{
foreach (var room in roomFlow)
- roomFlow.SetLayoutPosition(room, room.Room.Position.Value);
+ roomFlow.SetLayoutPosition(room, -(room.Room.RoomID.Value ?? 0));
}
protected override bool OnClick(ClickEvent e)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index 80a5daa7c8..0edf5dde6d 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -7,7 +7,6 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -99,14 +98,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex(@"28242d"),
+ Colour = colourProvider.Background4
},
new GridContainer
{
@@ -249,7 +248,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f),
+ Colour = colourProvider.Background5
},
new FillFlowContainer
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
index 6c3dfe7382..cf1066df10 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
@@ -79,11 +79,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void load()
{
isConnected.BindTo(client.IsConnected);
- isConnected.BindValueChanged(c => Scheduler.AddOnce(() =>
- {
- if (isConnected.Value && IsLoaded)
- PollImmediately();
- }), true);
+ isConnected.BindValueChanged(c => Scheduler.AddOnce(poll), true);
+ }
+
+ private void poll()
+ {
+ if (isConnected.Value && IsLoaded)
+ PollImmediately();
}
protected override Task Poll()
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
index 0f256160eb..a380ddef25 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
@@ -19,15 +19,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
- Client.RoomUpdated += OnRoomUpdated;
-
- Client.UserLeft += UserLeft;
- Client.UserKicked += UserKicked;
- Client.UserJoined += UserJoined;
+ Client.RoomUpdated += invokeOnRoomUpdated;
+ Client.UserLeft += invokeUserLeft;
+ Client.UserKicked += invokeUserKicked;
+ Client.UserJoined += invokeUserJoined;
OnRoomUpdated();
}
+ private void invokeOnRoomUpdated() => Scheduler.AddOnce(OnRoomUpdated);
+ private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.AddOnce(UserJoined, user);
+ private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(UserKicked, user);
+ private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.AddOnce(UserLeft, user);
+
///
/// Invoked when a user has joined the room.
///
@@ -63,10 +67,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
if (Client != null)
{
- Client.UserLeft -= UserLeft;
- Client.UserKicked -= UserKicked;
- Client.UserJoined -= UserJoined;
- Client.RoomUpdated -= OnRoomUpdated;
+ Client.RoomUpdated -= invokeOnRoomUpdated;
+ Client.UserLeft -= invokeUserLeft;
+ Client.UserKicked -= invokeUserKicked;
+ Client.UserJoined -= invokeUserJoined;
}
base.Dispose(isDisposing);
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
index 6f8c735b6e..79e305b765 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
@@ -15,6 +15,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
@@ -190,6 +191,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
+ if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating))
+ userModsDisplay.FadeIn(fade_time);
+ else
+ userModsDisplay.FadeOut(fade_time);
+
if (Client.IsHost && !User.Equals(Client.LocalUser))
kickButton.FadeIn(fade_time);
else
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
index 7384c60888..9e000aa712 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Specialized;
using Humanizer;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -77,14 +76,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex(@"28242d"),
+ Colour = colourProvider.Background4
},
new GridContainer
{
@@ -256,7 +255,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f),
+ Colour = colourProvider.Background5
},
new FillFlowContainer
{
diff --git a/osu.Game/Screens/Play/Break/BreakInfo.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs
index 6e129b20ea..6349ebd9a7 100644
--- a/osu.Game/Screens/Play/Break/BreakInfo.cs
+++ b/osu.Game/Screens/Play/Break/BreakInfo.cs
@@ -13,7 +13,9 @@ namespace osu.Game.Screens.Play.Break
public class BreakInfo : Container
{
public PercentageBreakInfoLine AccuracyDisplay;
- public BreakInfoLine RankDisplay;
+
+ // Currently unused but may be revisited in a future design update (see https://github.com/ppy/osu/discussions/15185)
+ // public BreakInfoLine RankDisplay;
public BreakInfoLine GradeDisplay;
public BreakInfo()
@@ -41,7 +43,9 @@ namespace osu.Game.Screens.Play.Break
Children = new Drawable[]
{
AccuracyDisplay = new PercentageBreakInfoLine("Accuracy"),
- RankDisplay = new BreakInfoLine("Rank"),
+
+ // See https://github.com/ppy/osu/discussions/15185
+ // RankDisplay = new BreakInfoLine("Rank"),
GradeDisplay = new BreakInfoLine("Grade"),
},
}
diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs
index e250791b72..242d997dd7 100644
--- a/osu.Game/Screens/Play/FailAnimation.cs
+++ b/osu.Game/Screens/Play/FailAnimation.cs
@@ -6,14 +6,17 @@ using osu.Framework.Bindables;
using osu.Game.Rulesets.UI;
using System;
using System.Collections.Generic;
+using ManagedBass.Fx;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Audio.Effects;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
@@ -22,27 +25,43 @@ namespace osu.Game.Screens.Play
{
///
/// Manage the animation to be applied when a player fails.
- /// Single file; automatically disposed after use.
+ /// Single use and automatically disposed after use.
///
- public class FailAnimation : CompositeDrawable
+ public class FailAnimation : Container
{
public Action OnComplete;
private readonly DrawableRuleset drawableRuleset;
-
private readonly BindableDouble trackFreq = new BindableDouble(1);
+ private Container filters;
+
+ private Box redFlashLayer;
+
private Track track;
private AudioFilter failLowPassFilter;
+ private AudioFilter failHighPassFilter;
private const float duration = 2500;
private Sample failSample;
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ protected override Container Content { get; } = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ };
+
public FailAnimation(DrawableRuleset drawableRuleset)
{
this.drawableRuleset = drawableRuleset;
+
+ RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
@@ -51,7 +70,26 @@ namespace osu.Game.Screens.Play
track = beatmap.Value.Track;
failSample = audio.Samples.Get(@"Gameplay/failsound");
- AddInternal(failLowPassFilter = new AudioFilter(audio.TrackMixer));
+ AddRangeInternal(new Drawable[]
+ {
+ filters = new Container
+ {
+ Children = new Drawable[]
+ {
+ failLowPassFilter = new AudioFilter(audio.TrackMixer),
+ failHighPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
+ },
+ },
+ Content,
+ redFlashLayer = new Box
+ {
+ Colour = Color4.Red,
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ Depth = float.MinValue,
+ Alpha = 0
+ },
+ });
}
private bool started;
@@ -66,21 +104,43 @@ namespace osu.Game.Screens.Play
started = true;
- failSample.Play();
-
this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ =>
{
OnComplete?.Invoke();
- Expire();
});
+ failHighPassFilter.CutoffTo(300);
failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic);
+ failSample.Play();
track.AddAdjustment(AdjustableProperty.Frequency, trackFreq);
applyToPlayfield(drawableRuleset.Playfield);
- drawableRuleset.Playfield.HitObjectContainer.FlashColour(Color4.Red, 500);
drawableRuleset.Playfield.HitObjectContainer.FadeOut(duration / 2);
+
+ if (config.Get(OsuSetting.FadePlayfieldWhenHealthLow))
+ redFlashLayer.FadeOutFromOne(1000);
+
+ Content.Masking = true;
+
+ Content.Add(new Box
+ {
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+ Depth = float.MaxValue
+ });
+
+ Content.ScaleTo(0.85f, duration, Easing.OutQuart);
+ Content.RotateTo(1, duration, Easing.OutQuart);
+ Content.FadeColour(Color4.Gray, duration);
+ }
+
+ public void RemoveFilters()
+ {
+ RemoveInternal(filters);
+ filters.Dispose();
+
+ track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
}
protected override void Update()
@@ -129,11 +189,5 @@ namespace osu.Game.Screens.Play
obj.MoveTo(originalPosition + new Vector2(0, 400), duration);
}
}
-
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
- track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
- }
}
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 090210e611..1381493fdf 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -220,6 +220,8 @@ namespace osu.Game.Screens.Play
// ensure the score is in a consistent state with the current player.
Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo;
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
+ if (ruleset.RulesetInfo.ID != null)
+ Score.ScoreInfo.RulesetID = ruleset.RulesetInfo.ID.Value;
Score.ScoreInfo.Mods = gameplayMods;
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score));
@@ -230,17 +232,53 @@ namespace osu.Game.Screens.Play
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
GameplayClockContainer.Add(rulesetSkinProvider);
- rulesetSkinProvider.AddRange(new[]
+ rulesetSkinProvider.AddRange(new Drawable[]
{
- // underlay and gameplay should have access to the skinning sources.
- createUnderlayComponents(),
- createGameplayComponents(Beatmap.Value, playableBeatmap)
+ failAnimationLayer = new FailAnimation(DrawableRuleset)
+ {
+ OnComplete = onFailComplete,
+ Children = new[]
+ {
+ // underlay and gameplay should have access to the skinning sources.
+ createUnderlayComponents(),
+ createGameplayComponents(Beatmap.Value, playableBeatmap)
+ }
+ },
+ FailOverlay = new FailOverlay
+ {
+ OnRetry = Restart,
+ OnQuit = () => PerformExit(true),
+ },
+ new HotkeyExitOverlay
+ {
+ Action = () =>
+ {
+ if (!this.IsCurrentScreen()) return;
+
+ fadeOut(true);
+ PerformExit(false);
+ },
+ },
});
+ if (Configuration.AllowRestart)
+ {
+ rulesetSkinProvider.Add(new HotkeyRetryOverlay
+ {
+ Action = () =>
+ {
+ if (!this.IsCurrentScreen()) return;
+
+ fadeOut(true);
+ Restart();
+ },
+ });
+ }
+
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
// also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
- rulesetSkinProvider.Add(createOverlayComponents(Beatmap.Value));
+ failAnimationLayer.Add(createOverlayComponents(Beatmap.Value));
if (!DrawableRuleset.AllowGameplayOverlays)
{
@@ -375,11 +413,6 @@ namespace osu.Game.Screens.Play
RequestSkip = () => progressToResults(false),
Alpha = 0
},
- FailOverlay = new FailOverlay
- {
- OnRetry = Restart,
- OnQuit = () => PerformExit(true),
- },
PauseOverlay = new PauseOverlay
{
OnResume = Resume,
@@ -387,18 +420,7 @@ namespace osu.Game.Screens.Play
OnRetry = Restart,
OnQuit = () => PerformExit(true),
},
- new HotkeyExitOverlay
- {
- Action = () =>
- {
- if (!this.IsCurrentScreen()) return;
-
- fadeOut(true);
- PerformExit(false);
- },
- },
- failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
- }
+ },
};
if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays)
@@ -410,20 +432,6 @@ namespace osu.Game.Screens.Play
if (GameplayClockContainer is MasterGameplayClockContainer master)
HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate;
- if (Configuration.AllowRestart)
- {
- container.Add(new HotkeyRetryOverlay
- {
- Action = () =>
- {
- if (!this.IsCurrentScreen()) return;
-
- fadeOut(true);
- Restart();
- },
- });
- }
-
return container;
}
@@ -541,7 +549,7 @@ namespace osu.Game.Screens.Play
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
if (ValidForResume && HasFailed)
{
- failAnimation.FinishTransforms(true);
+ failAnimationLayer.FinishTransforms(true);
return;
}
@@ -766,7 +774,7 @@ namespace osu.Game.Screens.Play
protected FailOverlay FailOverlay { get; private set; }
- private FailAnimation failAnimation;
+ private FailAnimation failAnimationLayer;
private bool onFail()
{
@@ -782,7 +790,7 @@ namespace osu.Game.Screens.Play
if (PauseOverlay.State.Value == Visibility.Visible)
PauseOverlay.Hide();
- failAnimation.Start();
+ failAnimationLayer.Start();
if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail))
Restart();
@@ -947,7 +955,7 @@ namespace osu.Game.Screens.Play
public override void OnSuspending(IScreen next)
{
- screenSuspension?.Expire();
+ screenSuspension?.RemoveAndDisposeImmediately();
fadeOut();
base.OnSuspending(next);
@@ -955,7 +963,8 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(IScreen next)
{
- screenSuspension?.Expire();
+ screenSuspension?.RemoveAndDisposeImmediately();
+ failAnimationLayer?.RemoveFilters();
// if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
if (prepareScoreForDisplayTask == null)
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 94a61a4ef3..d852ac2940 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play
{
protected const float BACKGROUND_BLUR = 15;
+ private const double content_out_duration = 300;
+
public override bool HideOverlaysOnEnter => hideOverlays;
public override bool DisallowExternalBeatmapRulesetChanges => true;
@@ -135,36 +137,39 @@ namespace osu.Game.Screens.Play
muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce);
- InternalChild = (content = new LogoTrackingContainer
+ InternalChildren = new Drawable[]
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- }).WithChildren(new Drawable[]
- {
- MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade)
+ (content = new LogoTrackingContainer
{
- Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- },
- PlayerSettings = new FillFlowContainer
+ RelativeSizeAxes = Axes.Both,
+ }).WithChildren(new Drawable[]
{
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 20),
- Margin = new MarginPadding(25),
- Children = new PlayerSettingsGroup[]
+ MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade)
{
- VisualSettings = new VisualSettings(),
- new InputSettings()
- }
- },
- idleTracker = new IdleTracker(750),
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ PlayerSettings = new FillFlowContainer
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 20),
+ Margin = new MarginPadding(25),
+ Children = new PlayerSettingsGroup[]
+ {
+ VisualSettings = new VisualSettings(),
+ new InputSettings()
+ }
+ },
+ idleTracker = new IdleTracker(750),
+ }),
lowPassFilter = new AudioFilter(audio.TrackMixer)
- });
+ };
if (Beatmap.Value.BeatmapInfo.EpilepsyWarning)
{
@@ -195,7 +200,6 @@ namespace osu.Game.Screens.Play
epilepsyWarning.DimmableBackground = b;
});
- lowPassFilter.CutoffTo(500, 100, Easing.OutCubic);
Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
content.ScaleTo(0.7f);
@@ -240,15 +244,18 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(IScreen next)
{
cancelLoad();
+ contentOut();
- content.ScaleTo(0.7f, 150, Easing.InQuint);
- this.FadeOut(150);
+ // If the load sequence was interrupted, the epilepsy warning may already be displayed (or in the process of being displayed).
+ epilepsyWarning?.Hide();
+
+ // Ensure the screen doesn't expire until all the outwards fade operations have completed.
+ this.Delay(content_out_duration).FadeOut();
ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
BackgroundBrightnessReduction = false;
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
- lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
return base.OnExiting(next);
}
@@ -344,6 +351,7 @@ namespace osu.Game.Screens.Play
content.FadeInFromZero(400);
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
+ lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint));
}
@@ -353,8 +361,9 @@ namespace osu.Game.Screens.Play
// Ensure the logo is no longer tracking before we scale the content
content.StopTracking();
- content.ScaleTo(0.7f, 300, Easing.InQuint);
- content.FadeOut(250);
+ content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint);
+ content.FadeOut(content_out_duration, Easing.OutQuint);
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration);
}
private void pushWhenLoaded()
@@ -381,7 +390,7 @@ namespace osu.Game.Screens.Play
contentOut();
- TransformSequence pushSequence = this.Delay(250);
+ TransformSequence pushSequence = this.Delay(content_out_duration);
// only show if the warning was created (i.e. the beatmap needs it)
// and this is not a restart of the map (the warning expires after first load).
@@ -400,6 +409,11 @@ namespace osu.Game.Screens.Play
})
.Delay(EpilepsyWarning.FADE_DURATION);
}
+ else
+ {
+ // This goes hand-in-hand with the restoration of low pass filter in contentOut().
+ this.TransformBindableTo(volumeAdjustment, 0, content_out_duration, Easing.OutCubic);
+ }
pushSequence.Schedule(() =>
{
diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs
index 8a4acacb24..26887327cd 100644
--- a/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
{
Nub.AccentColour = colours.Yellow;
Nub.GlowingAccentColour = colours.YellowLighter;
- Nub.GlowColour = colours.YellowDarker;
+ Nub.GlowColour = colours.YellowDark;
}
}
}
diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs
index c8e281195a..216e46d429 100644
--- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
AccentColour = colours.Yellow;
Nub.AccentColour = colours.Yellow;
Nub.GlowingAccentColour = colours.YellowLighter;
- Nub.GlowColour = colours.YellowDarker;
+ Nub.GlowColour = colours.YellowDark;
}
}
}
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 6cafcb9d16..a2dea355ac 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -410,7 +410,7 @@ namespace osu.Game.Screens.Select
{
if (e.NewValue is DummyWorkingBeatmap || !this.IsCurrentScreen()) return;
- Logger.Log($"working beatmap updated to {e.NewValue}");
+ Logger.Log($"Song select working beatmap updated to {e.NewValue}");
if (!Carousel.SelectBeatmap(e.NewValue.BeatmapInfo, false))
{
diff --git a/osu.Game/Skinning/ISkinSource.cs b/osu.Game/Skinning/ISkinSource.cs
index ba3e2bf6ad..a5ed0fc990 100644
--- a/osu.Game/Skinning/ISkinSource.cs
+++ b/osu.Game/Skinning/ISkinSource.cs
@@ -12,6 +12,9 @@ namespace osu.Game.Skinning
///
public interface ISkinSource : ISkin
{
+ ///
+ /// Fired whenever a source change occurs, signalling that consumers should re-query as required.
+ ///
event Action SourceChanged;
///
diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
index f5a7788359..b884794739 100644
--- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
+++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
@@ -58,10 +58,8 @@ namespace osu.Game.Skinning
return base.CreateChildDependencies(parent);
}
- protected override void OnSourceChanged()
+ protected override void RefreshSources()
{
- ResetSources();
-
// Populate a local list first so we can adjust the returned order as we go.
var sources = new List();
@@ -91,8 +89,7 @@ namespace osu.Game.Skinning
else
sources.Add(rulesetResourcesSkin);
- foreach (var skin in sources)
- AddSource(skin);
+ SetSources(sources);
}
protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin)
diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs
index ada6e4b788..c8e4c2c7b6 100644
--- a/osu.Game/Skinning/SkinProvidingContainer.cs
+++ b/osu.Game/Skinning/SkinProvidingContainer.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@@ -40,10 +41,12 @@ namespace osu.Game.Skinning
protected virtual bool AllowColourLookup => true;
+ private readonly object sourceSetLock = new object();
+
///
/// A dictionary mapping each source to a wrapper which handles lookup allowances.
///
- private readonly List<(ISkin skin, DisableableSkinSource wrapped)> skinSources = new List<(ISkin, DisableableSkinSource)>();
+ private (ISkin skin, DisableableSkinSource wrapped)[] skinSources = Array.Empty<(ISkin skin, DisableableSkinSource wrapped)>();
///
/// Constructs a new initialised with a single skin source.
@@ -52,7 +55,7 @@ namespace osu.Game.Skinning
: this()
{
if (skin != null)
- AddSource(skin);
+ SetSources(new[] { skin });
}
///
@@ -168,49 +171,42 @@ namespace osu.Game.Skinning
}
///
- /// Add a new skin to this provider. Will be added to the end of the lookup order precedence.
+ /// Replace the sources used for lookups in this container.
///
- /// The skin to add.
- protected void AddSource(ISkin skin)
+ ///
+ /// This does not implicitly fire a event. Consider calling if required.
+ ///
+ /// The new sources.
+ protected void SetSources(IEnumerable sources)
{
- skinSources.Add((skin, new DisableableSkinSource(skin, this)));
+ lock (sourceSetLock)
+ {
+ foreach (var skin in skinSources)
+ {
+ if (skin.skin is ISkinSource source)
+ source.SourceChanged -= TriggerSourceChanged;
+ }
- if (skin is ISkinSource source)
- source.SourceChanged += TriggerSourceChanged;
+ skinSources = sources.Select(skin => (skin, new DisableableSkinSource(skin, this))).ToArray();
+
+ foreach (var skin in skinSources)
+ {
+ if (skin.skin is ISkinSource source)
+ source.SourceChanged += TriggerSourceChanged;
+ }
+ }
}
///
- /// Remove a skin from this provider.
- ///
- /// The skin to remove.
- protected void RemoveSource(ISkin skin)
- {
- if (skinSources.RemoveAll(s => s.skin == skin) == 0)
- return;
-
- if (skin is ISkinSource source)
- source.SourceChanged -= TriggerSourceChanged;
- }
-
- ///
- /// Clears all skin sources.
- ///
- protected void ResetSources()
- {
- foreach (var i in skinSources.ToArray())
- RemoveSource(i.skin);
- }
-
- ///
- /// Invoked when any source has changed (either or a source registered via ).
+ /// Invoked after any consumed source change, before the external event is fired.
/// This is also invoked once initially during to ensure sources are ready for children consumption.
///
- protected virtual void OnSourceChanged() { }
+ protected virtual void RefreshSources() { }
protected void TriggerSourceChanged()
{
// Expose to implementations, giving them a chance to react before notifying external consumers.
- OnSourceChanged();
+ RefreshSources();
SourceChanged?.Invoke();
}
diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs
index e7125bb034..20c2fcc075 100644
--- a/osu.Game/Skinning/SkinnableTargetContainer.cs
+++ b/osu.Game/Skinning/SkinnableTargetContainer.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using System.Threading;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -22,6 +23,8 @@ namespace osu.Game.Skinning
public bool ComponentsLoaded { get; private set; }
+ private CancellationTokenSource cancellationSource;
+
public SkinnableTargetContainer(SkinnableTarget target)
{
Target = target;
@@ -38,6 +41,9 @@ namespace osu.Game.Skinning
content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer;
+ cancellationSource?.Cancel();
+ cancellationSource = null;
+
if (content != null)
{
LoadComponentAsync(content, wrapper =>
@@ -45,7 +51,7 @@ namespace osu.Game.Skinning
AddInternal(wrapper);
components.AddRange(wrapper.Children.OfType());
ComponentsLoaded = true;
- });
+ }, (cancellationSource = new CancellationTokenSource()).Token);
}
else
ComponentsLoaded = true;
diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs
new file mode 100644
index 0000000000..f7b7471634
--- /dev/null
+++ b/osu.Game/Stores/RealmFileStore.cs
@@ -0,0 +1,116 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.IO;
+using System.Linq;
+using osu.Framework.Extensions;
+using osu.Framework.IO.Stores;
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Framework.Testing;
+using osu.Game.Database;
+using osu.Game.Models;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Stores
+{
+ ///
+ /// Handles the storing of files to the file system (and database) backing.
+ ///
+ [ExcludeFromDynamicCompile]
+ public class RealmFileStore
+ {
+ private readonly RealmContextFactory realmFactory;
+
+ public readonly IResourceStore Store;
+
+ public readonly Storage Storage;
+
+ public RealmFileStore(RealmContextFactory realmFactory, Storage storage)
+ {
+ this.realmFactory = realmFactory;
+
+ Storage = storage.GetStorageForDirectory(@"files");
+ Store = new StorageBackedResourceStore(Storage);
+ }
+
+ ///
+ /// Add a new file to the game-wide database, copying it to permanent storage if not already present.
+ ///
+ /// The file data stream.
+ /// The realm instance to add to. Should already be in a transaction.
+ ///
+ public RealmFile Add(Stream data, Realm realm)
+ {
+ string hash = data.ComputeSHA2Hash();
+
+ var existing = realm.Find(hash);
+
+ var file = existing ?? new RealmFile { Hash = hash };
+
+ if (!checkFileExistsAndMatchesHash(file))
+ copyToStore(file, data);
+
+ if (!file.IsManaged)
+ realm.Add(file);
+
+ return file;
+ }
+
+ private void copyToStore(RealmFile file, Stream data)
+ {
+ data.Seek(0, SeekOrigin.Begin);
+
+ using (var output = Storage.GetStream(file.StoragePath, FileAccess.Write))
+ data.CopyTo(output);
+
+ data.Seek(0, SeekOrigin.Begin);
+ }
+
+ private bool checkFileExistsAndMatchesHash(RealmFile file)
+ {
+ string path = file.StoragePath;
+
+ // we may be re-adding a file to fix missing store entries.
+ if (!Storage.Exists(path))
+ return false;
+
+ // even if the file already exists, check the existing checksum for safety.
+ using (var stream = Storage.GetStream(path))
+ return stream.ComputeSHA2Hash() == file.Hash;
+ }
+
+ public void Cleanup()
+ {
+ var realm = realmFactory.Context;
+
+ // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal.
+ using (var transaction = realm.BeginWrite())
+ {
+ // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707)
+ var files = realm.All().ToList();
+
+ foreach (var file in files)
+ {
+ if (file.BacklinksCount > 0)
+ continue;
+
+ try
+ {
+ Storage.Delete(file.StoragePath);
+ realm.Remove(file);
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, $@"Could not delete databased file {file.Hash}");
+ }
+ }
+
+ transaction.Commit();
+ }
+ }
+ }
+}
diff --git a/osu.Game/Stores/RealmRulesetStore.cs b/osu.Game/Stores/RealmRulesetStore.cs
new file mode 100644
index 0000000000..27eb5d797f
--- /dev/null
+++ b/osu.Game/Stores/RealmRulesetStore.cs
@@ -0,0 +1,263 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using osu.Framework;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Game.Database;
+using osu.Game.Models;
+using osu.Game.Rulesets;
+
+#nullable enable
+
+namespace osu.Game.Stores
+{
+ public class RealmRulesetStore : IDisposable
+ {
+ private readonly RealmContextFactory realmFactory;
+
+ private const string ruleset_library_prefix = @"osu.Game.Rulesets";
+
+ private readonly Dictionary loadedAssemblies = new Dictionary();
+
+ ///
+ /// All available rulesets.
+ ///
+ public IEnumerable AvailableRulesets => availableRulesets;
+
+ private readonly List availableRulesets = new List();
+
+ public RealmRulesetStore(RealmContextFactory realmFactory, Storage? storage = null)
+ {
+ this.realmFactory = realmFactory;
+
+ // On android in release configuration assemblies are loaded from the apk directly into memory.
+ // We cannot read assemblies from cwd, so should check loaded assemblies instead.
+ loadFromAppDomain();
+
+ // This null check prevents Android from attempting to load the rulesets from disk,
+ // as the underlying path "AppContext.BaseDirectory", despite being non-nullable, it returns null on android.
+ // See https://github.com/xamarin/xamarin-android/issues/3489.
+ if (RuntimeInfo.StartupDirectory != null)
+ loadFromDisk();
+
+ // the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory.
+ // It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail
+ // to load as unable to locate the game core assembly.
+ AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly;
+
+ var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
+ if (rulesetStorage != null)
+ loadUserRulesets(rulesetStorage);
+
+ addMissingRulesets();
+ }
+
+ ///
+ /// Retrieve a ruleset using a known ID.
+ ///
+ /// The ruleset's internal ID.
+ /// A ruleset, if available, else null.
+ public IRulesetInfo? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id);
+
+ ///
+ /// Retrieve a ruleset using a known short name.
+ ///
+ /// The ruleset's short name.
+ /// A ruleset, if available, else null.
+ public IRulesetInfo? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName);
+
+ private Assembly? resolveRulesetDependencyAssembly(object? sender, ResolveEventArgs args)
+ {
+ var asm = new AssemblyName(args.Name);
+
+ // the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies.
+ // this attempts resolving the ruleset dependencies on game core and framework assemblies by returning assemblies with the same assembly name
+ // already loaded in the AppDomain.
+ var domainAssembly = AppDomain.CurrentDomain.GetAssemblies()
+ // Given name is always going to be equally-or-more qualified than the assembly name.
+ .Where(a =>
+ {
+ string? name = a.GetName().Name;
+ if (name == null)
+ return false;
+
+ return args.Name.Contains(name, StringComparison.Ordinal);
+ })
+ // Pick the greatest assembly version.
+ .OrderByDescending(a => a.GetName().Version)
+ .FirstOrDefault();
+
+ if (domainAssembly != null)
+ return domainAssembly;
+
+ return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName);
+ }
+
+ private void addMissingRulesets()
+ {
+ realmFactory.Context.Write(realm =>
+ {
+ var rulesets = realm.All();
+
+ List instances = loadedAssemblies.Values
+ .Select(r => Activator.CreateInstance(r) as Ruleset)
+ .Where(r => r != null)
+ .Select(r => r.AsNonNull())
+ .ToList();
+
+ // add all legacy rulesets first to ensure they have exclusive choice of primary key.
+ foreach (var r in instances.Where(r => r is ILegacyRuleset))
+ {
+ if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.ID) == null)
+ realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.ID));
+ }
+
+ // add any other rulesets which have assemblies present but are not yet in the database.
+ foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
+ {
+ if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
+ {
+ var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
+
+ if (existingSameShortName != null)
+ {
+ // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
+ // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
+ // in such cases, update the instantiation info of the existing entry to point to the new one.
+ existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
+ }
+ else
+ realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.ID));
+ }
+ }
+
+ List detachedRulesets = new List();
+
+ // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage.
+ foreach (var r in rulesets)
+ {
+ try
+ {
+ var type = Type.GetType(r.InstantiationInfo);
+
+ if (type == null)
+ throw new InvalidOperationException(@"Type resolution failure.");
+
+ var rInstance = (Activator.CreateInstance(type) as Ruleset)?.RulesetInfo;
+
+ if (rInstance == null)
+ throw new InvalidOperationException(@"Instantiation failure.");
+
+ r.Name = rInstance.Name;
+ r.ShortName = rInstance.ShortName;
+ r.InstantiationInfo = rInstance.InstantiationInfo;
+ r.Available = true;
+
+ detachedRulesets.Add(r.Clone());
+ }
+ catch (Exception ex)
+ {
+ r.Available = false;
+ Logger.Log($"Could not load ruleset {r}: {ex.Message}");
+ }
+ }
+
+ availableRulesets.AddRange(detachedRulesets);
+ });
+ }
+
+ private void loadFromAppDomain()
+ {
+ foreach (var ruleset in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ string? rulesetName = ruleset.GetName().Name;
+
+ if (rulesetName == null)
+ continue;
+
+ if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || rulesetName.Contains(@"Tests"))
+ continue;
+
+ addRuleset(ruleset);
+ }
+ }
+
+ private void loadUserRulesets(Storage rulesetStorage)
+ {
+ var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll");
+
+ foreach (var ruleset in rulesets.Where(f => !f.Contains(@"Tests")))
+ loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
+ }
+
+ private void loadFromDisk()
+ {
+ try
+ {
+ var files = Directory.GetFiles(RuntimeInfo.StartupDirectory, @$"{ruleset_library_prefix}.*.dll");
+
+ foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests")))
+ loadRulesetFromFile(file);
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, $"Could not load rulesets from directory {RuntimeInfo.StartupDirectory}");
+ }
+ }
+
+ private void loadRulesetFromFile(string file)
+ {
+ var filename = Path.GetFileNameWithoutExtension(file);
+
+ if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
+ return;
+
+ try
+ {
+ addRuleset(Assembly.LoadFrom(file));
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, $"Failed to load ruleset {filename}");
+ }
+ }
+
+ private void addRuleset(Assembly assembly)
+ {
+ if (loadedAssemblies.ContainsKey(assembly))
+ return;
+
+ // the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799).
+ // as a failsafe, also compare by FullName.
+ if (loadedAssemblies.Any(a => a.Key.FullName == assembly.FullName))
+ return;
+
+ try
+ {
+ loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset)));
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, $"Failed to add ruleset {assembly}");
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly;
+ }
+ }
+}
diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
index 64f1ee4a7a..6d63525011 100644
--- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
+++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
@@ -14,6 +14,7 @@ using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
+using osu.Game.IO.Serialization;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -109,6 +110,8 @@ namespace osu.Game.Tests.Beatmaps
{
var beatmap = GetBeatmap(name);
+ string beforeConversion = beatmap.Serialize();
+
var converterResult = new Dictionary>();
var working = new ConversionWorkingBeatmap(beatmap)
@@ -122,6 +125,10 @@ namespace osu.Game.Tests.Beatmaps
working.GetPlayableBeatmap(CreateRuleset().RulesetInfo, mods);
+ string afterConversion = beatmap.Serialize();
+
+ Assert.AreEqual(beforeConversion, afterConversion, "Conversion altered original beatmap");
+
return new ConvertResult
{
Mappings = converterResult.Select(r =>
diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs
index 34393fba7d..c2e9892735 100644
--- a/osu.Game/Tests/Visual/EditorClockTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Visual
@@ -23,7 +22,7 @@ namespace osu.Game.Tests.Visual
protected EditorClockTestScene()
{
- Clock = new EditorClock(new ControlPointInfo(), BeatDivisor) { IsCoupled = false };
+ Clock = new EditorClock(new Beatmap(), BeatDivisor) { IsCoupled = false };
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@@ -44,7 +43,7 @@ namespace osu.Game.Tests.Visual
private void beatmapChanged(ValueChangedEvent e)
{
- Clock.ControlPointInfo = e.NewValue.Beatmap.ControlPointInfo;
+ Clock.Beatmap = e.NewValue.Beatmap;
Clock.ChangeSource(e.NewValue.Track);
Clock.ProcessFrame();
}
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 2c0ca0b872..5e4e5942d9 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -53,7 +53,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
public MultiplayerRoomUser AddUser(User user, bool markAsPlaying = false)
{
var roomUser = new MultiplayerRoomUser(user.Id) { User = user };
- ((IMultiplayerClient)this).UserJoined(roomUser);
+
+ addUser(roomUser);
if (markAsPlaying)
PlayingUserIds.Add(user.Id);
@@ -61,7 +62,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
return roomUser;
}
- public void AddNullUser() => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(TestUserLookupCache.NULL_USER_ID));
+ public void AddNullUser() => addUser(new MultiplayerRoomUser(TestUserLookupCache.NULL_USER_ID));
+
+ private void addUser(MultiplayerRoomUser user)
+ {
+ ((IMultiplayerClient)this).UserJoined(user).Wait();
+
+ // We want the user to be immediately available for testing, so force a scheduler update to run the update-bound continuation.
+ Scheduler.Update();
+ }
public void RemoveUser(User user)
{
diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs
index 77db697cb6..6a11bd3fea 100644
--- a/osu.Game/Tests/Visual/OsuGameTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs
@@ -78,9 +78,11 @@ namespace osu.Game.Tests.Visual
protected void CreateGame()
{
- AddGame(Game = new TestOsuGame(LocalStorage, API));
+ AddGame(Game = CreateTestGame());
}
+ protected virtual TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API);
+
protected void PushAndConfirm(Func newScreen)
{
Screen screen = null;
@@ -135,7 +137,8 @@ namespace osu.Game.Tests.Visual
public new void PerformFromScreen(Action action, IEnumerable validScreens = null) => base.PerformFromScreen(action, validScreens);
- public TestOsuGame(Storage storage, IAPIProvider api)
+ public TestOsuGame(Storage storage, IAPIProvider api, string[] args = null)
+ : base(args)
{
Storage = storage;
API = api;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index ff382f5227..32d6eeab29 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,11 +36,12 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index fff0cbf418..92abab036a 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,8 +70,8 @@
-
-
+
+
@@ -93,7 +93,7 @@
-
+
diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings
index e42b30e944..3af986543e 100644
--- a/osu.sln.DotSettings
+++ b/osu.sln.DotSettings
@@ -73,6 +73,7 @@
HINT
WARNING
HINT
+ DO_NOT_SHOW
WARNING
DO_NOT_SHOW
WARNING