diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 0c6b80e97ef5..fc61573416e8 100755
--- 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 edbd24db82dd..39357f95f31f 100644
--- a/.github/workflows_/ci.yml
+++ b/.github/workflows_/ci.yml
@@ -86,9 +86,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 e0ccd50989b3..358cbda17a45 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 84fb0fe4dff8..b5396955a96b 100755
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 3c438dc7d7bc..043ba0b927ed 100755
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -22,10 +22,8 @@
using osu.Game.Updater;
using osu.Desktop.Windows;
using osu.Framework.Allocation;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Configuration;
-using osu.Game.Graphics;
using osu.Game.IO;
namespace osu.Desktop
@@ -34,7 +32,6 @@ internal class OsuGameDesktop : OsuGame
{
private readonly bool noVersionOverlay;
private VersionManager versionManager;
- private TextEditIndicator textEditIndicator;
private DBusManagerContainer dBusManagerContainer;
public OsuGameDesktop(string[] args = null)
@@ -119,8 +116,6 @@ protected override void LoadComplete()
if (!noVersionOverlay)
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
- LoadComponentAsync(textEditIndicator = new TextEditIndicator { Depth = int.MinValue }, Add);
-
LoadComponentAsync(new DiscordRichPresence(), Add);
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
@@ -167,33 +162,7 @@ public override void SetHost(GameHost host)
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
- desktopWindow.MinimumSize = new Size(600, 600);
-
- desktopWindow.OnTextEdit += s =>
- {
- if (textEditIndicator != null)
- {
- textEditIndicator.Text = s;
-
- if (textEditIndicator.State.Value != Visibility.Visible)
- Schedule(() => textEditIndicator.Show());
- }
- };
-
- desktopWindow.OnTextInput += () =>
- {
- if (textEditIndicator != null)
- {
- if (textEditIndicator.State.Value != Visibility.Hidden)
- {
- Schedule(() =>
- {
- textEditIndicator.Flash();
- textEditIndicator.Hide();
- });
- }
- }
- };
+ desktopWindow.MinimumSize = new Size(800, 600);
}
private readonly List importableFiles = new List();
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 518c3aa1aa75..4bb108207dd7 100755
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -9,7 +9,7 @@
osu!(lazer)
lazer.ico
app.manifest
- 2021.1006.0
+ 2021.1016.0
0.0.0
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
index 5580358f894c..2fab47f857da 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
@@ -14,11 +14,11 @@ public class CatchDifficultyCalculatorTest : DifficultyCalculatorTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
- [TestCase(4.050601681491468d, "diffcalc-test")]
+ [TestCase(4.0505463516206195d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(5.169743871843191d, "diffcalc-test")]
+ [TestCase(5.1696411260785498d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new CatchModDoubleTime());
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
index a45877155091..d4c2c0f0af49 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
@@ -29,8 +29,7 @@ public abstract class CatchSelectionBlueprintTestScene : SelectionBlueprintTestS
protected CatchSelectionBlueprintTestScene()
{
- EditorBeatmap = new EditorBeatmap(new CatchBeatmap());
- EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0;
+ EditorBeatmap = new EditorBeatmap(new CatchBeatmap()) { Difficulty = { CircleSize = 0 } };
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
{
BeatLength = 100
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs
index e3811b76692c..cca3701a6077 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs
@@ -5,6 +5,7 @@
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
@@ -26,7 +27,7 @@ public class TestSceneBananaShowerPlacementBlueprint : CatchPlacementBlueprintTe
protected override void AddHitObject(DrawableHitObject hitObject)
{
// Create nested bananas (but positions are not randomized because beatmap processing is not done).
- hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty);
+ hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
base.AddHitObject(hitObject);
}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs
index cd1fa31b619e..981efc9a1307 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs
@@ -4,9 +4,9 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -23,11 +23,12 @@ public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTes
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
- [BackgroundDependencyLoader]
- private void load()
+ protected override IBeatmap GetPlayableBeatmap()
{
- Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderTickRate = 5;
- Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity * 10;
+ var playable = base.GetPlayableBeatmap();
+ playable.Difficulty.SliderTickRate = 5;
+ playable.Difficulty.SliderMultiplier = velocity * 10;
+ return playable;
}
[Test]
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
index 5e73a89069a0..155d033dd059 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
@@ -223,7 +223,7 @@ private void addBlueprintStep(double time, float x, SliderPath sliderPath, doubl
X = x,
Path = sliderPath,
};
- EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity;
+ EditorBeatmap.Difficulty.SliderMultiplier = velocity;
EditorBeatmap.Add(hitObject);
EditorBeatmap.Update(hitObject);
Assert.That(hitObject.Velocity, Is.EqualTo(velocity));
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index 93596214db73..850e76d4bec2 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -211,7 +211,7 @@ private static void initialiseHyperDash(IBeatmap beatmap)
palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
- double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
+ double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2;
// Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins.
// This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible.
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index f155ee594992..3f41ece95e56 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -34,7 +34,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
return new CatchDifficultyAttributes { Mods = mods, Skills = skills };
// this is the same as osu!, so there's potential to share the implementation... maybe
- double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
+ double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
return new CatchDifficultyAttributes
{
@@ -69,10 +69,10 @@ protected override IEnumerable CreateDifficultyHitObjects(I
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
- halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
+ halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
- halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f);
+ halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f);
return new Skill[]
{
diff --git a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
index 0344709d4564..9a7528d90c46 100644
--- a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
+++ b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
@@ -16,6 +16,6 @@ public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyLi
{
}
- protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
+ protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 9fee4ae2a6b5..3737eb2cfcdd 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -42,9 +42,8 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I
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.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index ba6e9224c9e9..a8ec9f1d2f4c 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -27,14 +27,14 @@ public DrawableCatchRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList new CatchFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
- protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
+ protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.Difficulty);
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index 538a51db5fc0..5ccb191a9b42 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.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 @@ public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screen
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 471dad87d541..4387bc6b3b18 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -388,7 +388,7 @@ private void performTest(List frames, Beatmap beatm
},
};
- 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 18891f8c5890..89e13acad6d4 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -148,7 +148,7 @@ private void performTest(List hitObjects, List fram
},
});
- 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/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 93b5a802d7ce..0fcdd890fc93 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -42,8 +42,8 @@ public ManiaBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
{
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
- var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
- var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+ var roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize);
+ var roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty);
if (IsForCurrentRuleset)
{
@@ -81,7 +81,7 @@ public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo)
protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
- IBeatmapDifficultyInfo difficulty = original.BeatmapInfo.BaseDifficulty;
+ IBeatmapDifficultyInfo difficulty = original.Difficulty;
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new FastRandom(seed);
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index d0232dde9d88..ae527516e8ee 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 @@ public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, Ma
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,13 +55,13 @@ public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, Ma
#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);
// This matches stable's calculation.
- EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier);
+ EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier);
SegmentDuration = (EndTime - StartTime) / SpanCount;
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
index d65e78bb490e..eaf0ea0f2b88 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
@@ -111,7 +111,7 @@ protected double ConversionDifficulty
if (drainTime == 0)
drainTime = 10000;
- IBeatmapDifficultyInfo difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
+ IBeatmapDifficultyInfo difficulty = OriginalBeatmap.Difficulty;
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index fc29eadedc58..aee3268544f7 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -41,14 +41,13 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
return new ManiaDifficultyAttributes { Mods = mods, Skills = skills };
HitWindows hitWindows = new ManiaHitWindows();
- hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+ hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
return new ManiaDifficultyAttributes
{
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods,
- // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
+ GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate),
ScoreMultiplier = getScoreMultiplier(mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
index 7c75b45e5ffb..ca9e5b0b8539 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 b469487c8f2c..310d97730637 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -90,11 +90,11 @@ private void load()
{
// Mania doesn't care about global velocity
p.Velocity = 1;
- p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
+ p.BaseBeatLength *= Beatmap.Difficulty.SliderMultiplier;
// 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 9af2a994709d..ef43c3a6968b 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.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;
@@ -45,7 +46,7 @@ public TestSceneOsuDistanceSnapGrid()
[SetUp]
public void Setup() => Schedule(() =>
{
- editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
+ editorBeatmap.Difficulty.SliderMultiplier = 1;
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
@@ -179,15 +180,15 @@ public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosi
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 0ba775e5c7fb..37f1a846adbc 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -45,8 +45,8 @@ private void runSpmTest(Mod mod)
{
new Spinner
{
- Duration = 2000,
- Position = OsuPlayfield.BASE_SIZE / 2
+ Duration = 6000,
+ Position = OsuPlayfield.BASE_SIZE / 2,
}
}
},
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
index af64be78f818..ed9da36b05b2 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
@@ -120,7 +120,7 @@ public void TestWithSliderReuse() => CreateModTest(new ModTestData
private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4;
private bool objectWithIncreasedVisibilityHasIndex(int index)
- => Player.Mods.Value.OfType().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index];
+ => Player.GameplayState.Mods.OfType().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index];
private class TestOsuModHidden : OsuModHidden
{
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 19881b5c33af..15675e74d11b 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,13 +15,13 @@ public class OsuDifficultyCalculatorTest : DifficultyCalculatorTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.6634445062299665d, "diffcalc-test")]
- [TestCase(1.0414203870195022d, "zero-length-sliders")]
+ [TestCase(6.5867229481955389d, "diffcalc-test")]
+ [TestCase(1.0416315570967911d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(8.3858089051603368d, "diffcalc-test")]
- [TestCase(1.2723279173428435d, "zero-length-sliders")]
+ [TestCase(8.2730989071947896d, "diffcalc-test")]
+ [TestCase(1.2726413186221039d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 41d9bf713280..1f01ba601b0a 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -59,8 +59,8 @@ public TestSceneGameplayCursor()
AddSliderStep("circle size", 0f, 10f, 0f, val =>
{
config.SetValue(OsuSetting.AutoCursorSize, true);
- gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
- Scheduler.AddOnce(() => loadContent(false));
+ gameplayState.Beatmap.Difficulty.CircleSize = val;
+ Scheduler.AddOnce(loadContent);
});
AddStep("test cursor container", () => loadContent(false));
@@ -75,10 +75,10 @@ public TestSceneGameplayCursor()
public void TestSizing(int circleSize, float userScale)
{
AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
- AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
+ 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 @@ public void TestTopLeftOrigin()
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/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
index af67ab5839d7..a5629119b615 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
@@ -32,7 +32,7 @@ public void TestMissViaEarlyHit()
};
var hitWindows = new OsuHitWindows();
- hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+ hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
CreateModTest(new ModTestData
{
@@ -55,7 +55,7 @@ public void TestMissViaNotHitting()
};
var hitWindows = new OsuHitWindows();
- hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+ hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
CreateModTest(new ModTestData
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
index cfce80a2b214..d31e7a31f517 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
@@ -400,15 +400,13 @@ private void performTest(List hitObjects, List frames
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
HitObjects = hitObjects,
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 3 },
BeatmapInfo =
{
- BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
Ruleset = new OsuRuleset().RulesetInfo
},
});
- 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 @@ private class TestSlider : Slider
{
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 81902c25af10..03b4254eedb6 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -13,6 +13,7 @@
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 @@ private Drawable createCatmull(int repeats = 0)
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 590d15930060..f3392724ec3d 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -348,6 +348,7 @@ private void performTest(List frames)
{
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 @@ private void performTest(List frames)
},
});
- 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 fd4494ea6cf4..a243aba27ab6 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -29,6 +29,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; }
@@ -68,7 +71,7 @@ public void TestSpinnerMiddleRewindingRotation()
{
double finalAbsoluteDiscRotation = 0, finalRelativeDiscRotation = 0, finalSpinnerSymbolRotation = 0;
- addSeekStep(5000);
+ addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
{
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
@@ -81,8 +84,8 @@ public void TestSpinnerMiddleRewindingRotation()
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
- addSeekStep(2500);
- AddUntilStep("disc rotation rewound",
+ 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%
// (5% relative to the final rotation value, but we're half-way through the spin).
@@ -93,7 +96,7 @@ public void TestSpinnerMiddleRewindingRotation()
// 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, finalRelativeDiscRotation, 100));
AddAssert("is symbol rotation almost same",
@@ -131,7 +134,7 @@ public void TestRotationDirection([Values(true, false)] bool clockwise)
[Test]
public void TestSpinnerNormalBonusRewinding()
{
- addSeekStep(1000);
+ addSeekStep(spinner_start_time + 1000);
AddAssert("player score matching expected bonus score", () =>
{
@@ -192,24 +195,9 @@ public void TestSpinUnaffectedByClockRate(double rate)
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));
}
@@ -232,7 +220,8 @@ private void transformReplay(Func replayTransformation) => AddSt
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 1b85e0efdeeb..2d43e1b95ec3 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
@@ -369,8 +369,6 @@ private void performTest(List hitObjects, List frames
},
});
- 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 @@ private class TestSlider : Slider
{
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 a2fc4848af82..d82186fb5273 100755
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
@@ -11,6 +11,7 @@
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 @@ protected override IEnumerable ConvertHitObject(HitObject original
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 h is OsuModRelax))
+ speedRating = 0.0;
+
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
double baseFlashlightPerformance = 0.0;
@@ -53,7 +56,8 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
- double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
+ double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
+ double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
@@ -71,6 +75,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
FlashlightRating = flashlightRating,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
+ DrainRate = drainRate,
MaxCombo = maxCombo,
HitCircleCount = hitCirclesCount,
SpinnerCount = spinnerCount,
@@ -95,10 +100,9 @@ protected override IEnumerable CreateDifficultyHitObjects(I
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
HitWindows hitWindows = new OsuHitWindows();
- hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+ hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
- // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
+ hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
return new Skill[]
{
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 9cc8aeb52c66..d17651904583 100755
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -40,15 +40,21 @@ public override double Calculate(Dictionary categoryRatings = nu
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
- // Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
+ // Custom multipliers for NoFail and SpunOut.
if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
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;
+ multiplier *= 0.6;
+ }
+
double aimValue = computeAimValue();
double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue();
@@ -108,9 +114,13 @@ private double computeAimValue()
double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
- // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
- if (mods.Any(h => h is OsuModHidden))
+ if (mods.Any(m => m is OsuModBlinds))
+ aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
+ else if (mods.Any(h => h is OsuModHidden))
+ {
+ // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
+ }
aimValue *= approachRateBonus;
@@ -147,11 +157,20 @@ private double computeSpeedValue()
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
- if (mods.Any(m => m is OsuModHidden))
+ if (mods.Any(m => m is OsuModBlinds))
+ {
+ // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
+ speedValue *= 1.12;
+ }
+ else if (mods.Any(m => m is OsuModHidden))
+ {
+ // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
+ }
// Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
+
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@@ -160,6 +179,9 @@ private double computeSpeedValue()
private double computeAccuracyValue()
{
+ if (mods.Any(h => h is OsuModRelax))
+ return 0.0;
+
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
@@ -180,8 +202,12 @@ private double computeAccuracyValue()
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
- if (mods.Any(m => m is OsuModHidden))
+ // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
+ if (mods.Any(m => m is OsuModBlinds))
+ accuracyValue *= 1.14;
+ else if (mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08;
+
if (mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 8e8f9bc06eb4..5e5993aefec5 100755
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -54,6 +54,10 @@ public OsuDifficultyHitObject(HitObject hitObject, HitObject lastLastObject, Hit
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 @@ private void setDistances()
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/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 16a18cbcb90c..d8f4aa1229f2 100755
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -22,17 +22,19 @@ public Aim(Mod[] mods)
{
}
- protected override double SkillMultiplier => 26.25;
- protected override double StrainDecayBase => 0.15;
+ private double currentStrain = 1;
- protected override double StrainValueOf(DifficultyHitObject current)
+ private double skillMultiplier => 26.25;
+ private double strainDecayBase => 0.15;
+
+ private double strainValueOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
var osuCurrent = (OsuDifficultyHitObject)current;
- double result = 0;
+ double aimStrain = 0;
if (Previous.Count > 0)
{
@@ -46,7 +48,7 @@ protected override double StrainValueOf(DifficultyHitObject current)
Math.Max(osuPrevious.JumpDistance - scale, 0)
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
* Math.Max(osuCurrent.JumpDistance - scale, 0));
- result = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
+ aimStrain = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
}
}
@@ -54,11 +56,23 @@ protected override double StrainValueOf(DifficultyHitObject current)
double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance);
return Math.Max(
- result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
+ aimStrain + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
(Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime
);
}
private double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
+
+ private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
+
+ protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
+
+ protected override double StrainValueAt(DifficultyHitObject current)
+ {
+ currentStrain *= strainDecay(current.DeltaTime);
+ currentStrain += strainValueOf(current) * skillMultiplier;
+
+ return currentStrain;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
index abd900a80d53..e3abe7d7008b 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
@@ -19,12 +19,13 @@ public Flashlight(Mod[] mods)
{
}
- protected override double SkillMultiplier => 0.15;
- protected override double StrainDecayBase => 0.15;
+ private double skillMultiplier => 0.15;
+ private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0;
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
+ private double currentStrain = 1;
- protected override double StrainValueOf(DifficultyHitObject current)
+ private double strainValueOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
@@ -62,5 +63,17 @@ protected override double StrainValueOf(DifficultyHitObject current)
return Math.Pow(smallDistNerf * result, 2.0);
}
+
+ private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
+
+ protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
+
+ protected override double StrainValueAt(DifficultyHitObject current)
+ {
+ currentStrain *= strainDecay(current.DeltaTime);
+ currentStrain += strainValueOf(current) * skillMultiplier;
+
+ return currentStrain;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
index 7bcd867a9c28..e47edc37cca9 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
@@ -10,7 +10,7 @@
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
- public abstract class OsuStrainSkill : StrainDecaySkill
+ public abstract class OsuStrainSkill : StrainSkill
{
///
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 9364b110484b..cae6b8e01cbf 100755
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -16,18 +16,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public class Speed : OsuStrainSkill
{
private const double single_spacing_threshold = 125;
+ private const double rhythm_multiplier = 0.75;
+ private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
+ private const double min_speed_bonus = 75; // ~200BPM
+ private const double speed_balancing_factor = 40;
+
+ private double skillMultiplier => 1375;
+ private double strainDecayBase => 0.3;
- private const double angle_bonus_begin = 5 * Math.PI / 6;
- private const double pi_over_4 = Math.PI / 4;
- private const double pi_over_2 = Math.PI / 2;
+ private double currentStrain = 1;
+ private double currentRhythm = 1;
- protected override double SkillMultiplier => 1400;
- protected override double StrainDecayBase => 0.3;
protected override int ReducedSectionCount => 5;
protected override double DifficultyMultiplier => 1.04;
-
- private const double min_speed_bonus = 75; // ~200BPM
- private const double speed_balancing_factor = 40;
+ protected override int HistoryLength => 32;
private readonly double greatWindow;
@@ -37,52 +39,138 @@ public Speed(Mod[] mods, double hitWindowGreat)
greatWindow = hitWindowGreat;
}
- protected override double StrainValueOf(DifficultyHitObject current)
+ ///
+ /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current .
+ ///
+ private double calculateRhythmBonus(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
- var osuCurrent = (OsuDifficultyHitObject)current;
- var osuPrevious = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
+ int previousIslandSize = 0;
+
+ double rhythmComplexitySum = 0;
+ int islandSize = 1;
+ double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
+
+ bool firstDeltaSwitch = false;
+
+ for (int i = Previous.Count - 2; i > 0; i--)
+ {
+ OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1];
+ OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i];
+ OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1];
+
+ double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now
+
+ if (currHistoricalDecay != 0)
+ {
+ currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
+
+ double currDelta = currObj.StrainTime;
+ double prevDelta = prevObj.StrainTime;
+ double lastDelta = lastObj.StrainTime;
+ double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
+
+ double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
+
+ windowPenalty = Math.Min(1, windowPenalty);
+
+ double effectiveRatio = windowPenalty * currRatio;
+
+ if (firstDeltaSwitch)
+ {
+ if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
+ {
+ if (islandSize < 7)
+ islandSize++; // island is still progressing, count size.
+ }
+ else
+ {
+ if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
+ effectiveRatio *= 0.125;
+
+ if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
+ effectiveRatio *= 0.25;
+
+ if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
+ effectiveRatio *= 0.25;
+
+ if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
+ effectiveRatio *= 0.50;
+
+ if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
+ effectiveRatio *= 0.125;
+
+ rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
- double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance);
- double strainTime = osuCurrent.StrainTime;
+ startRatio = effectiveRatio;
+ previousIslandSize = islandSize; // log the last island size.
+
+ if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
+ firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
+
+ islandSize = 1;
+ }
+ }
+ else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
+ {
+ // Begin counting island until we change speed again.
+ firstDeltaSwitch = true;
+ startRatio = effectiveRatio;
+ islandSize = 1;
+ }
+ }
+ }
+
+ return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
+ }
+
+ private double strainValueOf(DifficultyHitObject current)
+ {
+ if (current.BaseObject is Spinner)
+ return 0;
+
+ // derive strainTime for calculation
+ var osuCurrObj = (OsuDifficultyHitObject)current;
+ var osuPrevObj = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
+
+ double strainTime = osuCurrObj.StrainTime;
double greatWindowFull = greatWindow * 2;
double speedWindowRatio = strainTime / greatWindowFull;
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
- if (osuPrevious != null && strainTime < greatWindowFull && osuPrevious.StrainTime > strainTime)
- strainTime = Interpolation.Lerp(osuPrevious.StrainTime, strainTime, speedWindowRatio);
+ if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime)
+ strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio);
// Cap deltatime to the OD 300 hitwindow.
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
+ // derive speedBonus for calculation
double speedBonus = 1.0;
+
if (strainTime < min_speed_bonus)
- speedBonus = 1 + Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
+ speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
- double angleBonus = 1.0;
+ double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance);
- if (osuCurrent.Angle != null && osuCurrent.Angle.Value < angle_bonus_begin)
- {
- angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57;
+ return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
+ }
- if (osuCurrent.Angle.Value < pi_over_2)
- {
- angleBonus = 1.28;
- if (distance < 90 && osuCurrent.Angle.Value < pi_over_4)
- angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1);
- else if (distance < 90)
- angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - osuCurrent.Angle.Value) / pi_over_4);
- }
- }
+ private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
+
+ protected override double CalculateInitialStrain(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime);
+
+ protected override double StrainValueAt(DifficultyHitObject current)
+ {
+ currentStrain *= strainDecay(current.DeltaTime);
+ currentStrain += strainValueOf(current) * skillMultiplier;
+
+ currentRhythm = calculateRhythmBonus(current);
- return (1 + (speedBonus - 1) * 0.75)
- * angleBonus
- * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5))
- / strainTime;
+ return currentStrain * currentRhythm;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index cf771b7ceabb..974b5e792546 100755
--- 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.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 @@ protected override void LoadComplete()
inputManager = GetContainingInputManager();
}
+ [Resolved]
+ private EditorBeatmap editorBeatmap { get; set; }
+
public override void UpdateTimeAndPosition(SnapResult result)
{
base.UpdateTimeAndPosition(result);
@@ -75,6 +80,10 @@ public override void UpdateTimeAndPosition(SnapResult result)
{
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 @@ private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint)
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 f684a32df713..95094886ff16 100755
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -230,7 +230,7 @@ private void removeControlPoints(List toRemove)
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/Checks/CheckTooShortSpinners.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs
index 0d0c3d9e6944..f0aade1b7fc8 100644
--- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs
@@ -19,7 +19,7 @@ public class CheckTooShortSpinners : ICheck
public IEnumerable Run(BeatmapVerifierContext context)
{
- double od = context.Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
+ double od = context.Beatmap.Difficulty.OverallDifficulty;
// These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner.
// It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners.
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
index ff3be974273e..8a561f962ab5 100755
--- 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 000000000000..c48cbd999292
--- /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/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
index 9ef2aab7ef44..d70abfcf5f65 100755
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
@@ -175,7 +175,7 @@ public override void ApplyToBeatmap(IBeatmap beatmap)
.Select(beat =>
{
var newCircle = new HitCircle();
- newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty);
+ newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.Difficulty);
newCircle.StartTime = beat;
return (OsuHitObject)newCircle;
}).ToList();
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 1d2666f46bbb..07d03ee1ebb0 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -140,9 +140,8 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I
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 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok
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 0ae6e84b118e..f42c4d821d83 100755
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -193,6 +193,7 @@ public override IEnumerable GetModsFor(ModType type)
new OsuModBarrelRoll(),
new OsuModApproachDifferent(),
new OsuModMuted(),
+ new OsuModNoScope(),
};
case ModType.System:
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
index b88bf9108bb1..e231550e3ebd 100755
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
@@ -50,7 +50,7 @@ public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList mods)
: base(beatmap, mods)
{
defaultHitWindows = new OsuHitWindows();
- defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+ defaultHitWindows.SetDifficulty(Beatmap.Difficulty.OverallDifficulty);
}
#endregion
diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
index cb769c31b83c..24a660e69e48 100644
--- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
+++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
@@ -179,7 +179,7 @@ private void load()
return;
// Todo: This should probably not be done like this.
- float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2;
+ float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.Difficulty.CircleSize - 5) / 5) / 2;
foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
{
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
index 437599d4976b..5bcdb6a97774 100755
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
@@ -99,7 +99,7 @@ private void calculateScale()
if (autoCursorScale.Value && state != null)
{
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
- scale *= GetScaleForCircleSize(state.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
+ scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize);
}
cursorScale.Value = scale;
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index dd3c6b317ad4..4b0b74ad2743 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ public class TaikoDifficultyCalculatorTest : DifficultyCalculatorTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(2.2867022617692685d, "diffcalc-test")]
- [TestCase(2.2867022617692685d, "diffcalc-test-strong")]
+ [TestCase(2.2420075288523802d, "diffcalc-test")]
+ [TestCase(2.2420075288523802d, "diffcalc-test-strong")]
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(3.1704781712282624d, "diffcalc-test")]
- [TestCase(3.1704781712282624d, "diffcalc-test-strong")]
+ [TestCase(3.134084469440479d, "diffcalc-test")]
+ [TestCase(3.134084469440479d, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index d6a69a49af88..79948859e854 100755
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -10,6 +10,7 @@
using System.Linq;
using osu.Framework.Utils;
using System.Threading;
+using JetBrains.Annotations;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
@@ -46,11 +47,10 @@ public TaikoBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
- if (!(original.BeatmapInfo.BaseDifficulty is TaikoMutliplierAppliedDifficulty))
+ if (!(original.Difficulty is TaikoMultiplierAppliedDifficulty))
{
// Rewrite the beatmap info to add the slider velocity multiplier
- original.BeatmapInfo = original.BeatmapInfo.Clone();
- original.BeatmapInfo.BaseDifficulty = new TaikoMutliplierAppliedDifficulty(original.BeatmapInfo.BaseDifficulty);
+ original.Difficulty = new TaikoMultiplierAppliedDifficulty(original.Difficulty);
}
Beatmap converted = base.ConvertBeatmap(original, cancellationToken);
@@ -108,7 +108,7 @@ protected override IEnumerable ConvertHitObject(HitObject obj, I
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = taikoDuration,
- TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4
+ TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4
};
}
@@ -117,7 +117,7 @@ protected override IEnumerable ConvertHitObject(HitObject obj, I
case IHasDuration endTimeData:
{
- double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
+ double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
yield return new Swell
{
@@ -154,7 +154,7 @@ private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDist
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,12 +162,12 @@ private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDist
#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.BeatmapInfo.BaseDifficulty.SliderMultiplier / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate;
+ double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;
// The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
- double taikoVelocity = sliderScoringPointDistance * beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate;
+ double taikoVelocity = sliderScoringPointDistance * beatmap.Difficulty.SliderTickRate;
taikoDuration = (int)(distance / taikoVelocity * beatLength);
if (isForCurrentRuleset)
@@ -183,7 +183,7 @@ private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDist
beatLength = timingPoint.BeatLength;
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
- tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, (double)taikoDuration / spans);
+ tickSpacing = Math.Min(beatLength / beatmap.Difficulty.SliderTickRate, (double)taikoDuration / spans);
return tickSpacing > 0
&& distance / osuVelocity * 1000 < 2 * beatLength;
@@ -191,14 +191,35 @@ private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDist
protected override Beatmap CreateBeatmap() => new TaikoBeatmap();
- private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty
+ private class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
{
- public TaikoMutliplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
+ public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
{
CopyFrom(difficulty);
+ }
+
+ [UsedImplicitly]
+ public TaikoMultiplierAppliedDifficulty()
+ {
+ }
+
+ #region Overrides of BeatmapDifficulty
- SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
+ public override void CopyTo(BeatmapDifficulty other)
+ {
+ base.CopyTo(other);
+ if (!(other is TaikoMultiplierAppliedDifficulty))
+ SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
+
+ public override void CopyFrom(IBeatmapDifficultyInfo other)
+ {
+ base.CopyFrom(other);
+ if (!(other is TaikoMultiplierAppliedDifficulty))
+ SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
+ }
+
+ #endregion
}
}
}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 18d06c069fa4..7dd47e804b27 100755
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -85,7 +85,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
starRating = rescale(starRating);
HitWindows hitWindows = new TaikoHitWindows();
- hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+ hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
return new TaikoDifficultyAttributes
{
@@ -94,8 +94,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
StaminaStrain = staminaRating,
RhythmStrain = rhythmRating,
ColourStrain = colourRating,
- // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate,
+ GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
Skills = skills
};
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index 0318e329911b..0e93ad7e73ba 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -63,9 +63,8 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I
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.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs
index 94cd411d7be0..0d6ce44255ce 100755
--- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs
@@ -40,8 +40,8 @@ public override void ApplyBeatmap(IBeatmap beatmap)
{
base.ApplyBeatmap(beatmap);
- hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98));
- hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120);
+ hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.5, 0.75, 0.98));
+ hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.0018, 0.0075, 0.0120);
}
protected override double GetHealthIncreaseFor(JudgementResult result)
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index a4bf8c92e38a..cb12d0362002 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -129,7 +129,7 @@ public void TestDecodeBeatmapDifficulty()
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new LineBufferedReader(resStream))
{
- var difficulty = decoder.Decode(stream).BeatmapInfo.BaseDifficulty;
+ var difficulty = decoder.Decode(stream).Difficulty;
Assert.AreEqual(6.5f, difficulty.DrainRate);
Assert.AreEqual(4, difficulty.CircleSize);
@@ -192,15 +192,15 @@ public void TestDecodeBeatmapTimingPoints()
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 @@ public void TestDecodeBeatmapTimingPoints()
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 @@ public void TestDecodeOverlappingTimingPoints()
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 @@ public void TestTimingPointResetsSpeedMultiplier()
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 @@ public void TestDecodeControlPointDifficultyChange()
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 896aa53f8297..d12da1a22f42 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -46,8 +46,7 @@ public void TestEncodeDecodeStability(string name)
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 @@ public void TestEncodeDecodeStabilityDoubleConvert(string name)
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 @@ public void TestEncodeDecodeStabilityWithNonLegacyControlPoints(string name)
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 @@ ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo
// 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 @@ ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo
}
}
+ 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 @@ private void sort(IBeatmap beatmap)
}
}
- 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 @@ public TestLegacySkin(IResourceStore storage, string fileName)
}
}
- 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/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
index 1fc3abef9ad4..9ec2f375694b 100644
--- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
@@ -84,7 +84,7 @@ public void TestDecodeEditor()
public void TestDecodeDifficulty()
{
var beatmap = decodeAsJson(normal);
- var difficulty = beatmap.BeatmapInfo.BaseDifficulty;
+ var difficulty = beatmap.Difficulty;
Assert.AreEqual(6.5f, difficulty.DrainRate);
Assert.AreEqual(4, difficulty.CircleSize);
Assert.AreEqual(8, difficulty.OverallDifficulty);
@@ -102,7 +102,7 @@ public void TestDecodePostConverted()
processor.PreProcess();
foreach (var o in converted.HitObjects)
- o.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty);
+ o.ApplyDefaults(converted.ControlPointInfo, converted.Difficulty);
processor.PostProcess();
var beatmap = converted.Serialize().Deserialize();
diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index a147b3ceb622..9a9ecd2a3708 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -513,5 +513,17 @@ public void TestRelativeExternalLinks()
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 000000000000..861de5303d1f
--- /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 245981cd9b62..3e8b6091fdfd 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 000000000000..33aa1afb89da
--- /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 576f901c1ae9..04c9f2577a10 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 @@ static RealmTest()
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);
+
+ using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ {
+ Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
+ await testAction(realmFactory, testStorage);
- realmFactory.Dispose();
+ realmFactory.Dispose();
- Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
- realmFactory.Compact();
- Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
+ Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
+ realmFactory.Compact();
+ }
+ }));
+ }
+ }
+
+ protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset)
+ {
+ RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() };
+
+ var metadata = new RealmBeatmapMetadata
+ {
+ Title = "My Love",
+ Artist = "Kuba Oms"
+ };
+
+ var beatmapSet = new RealmBeatmapSet
+ {
+ Beatmaps =
+ {
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", }
+ },
+ Files =
+ {
+ new RealmNamedFileUsage(createRealmFile(), "test [easy].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [normal].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [hard].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [insane].osu"),
}
- });
+ };
+
+ for (int i = 0; i < 8; i++)
+ beatmapSet.Files.Add(new RealmNamedFileUsage(createRealmFile(), $"hitsound{i}.mp3"));
+
+ foreach (var b in beatmapSet.Beatmaps)
+ b.BeatmapSet = beatmapSet;
+
+ return beatmapSet;
+ }
+
+ protected static RealmRuleset CreateRuleset() =>
+ new RealmRuleset(0, "osu!", "osu", true);
+
+ private class RealmTestGame : Framework.Game
+ {
+ public RealmTestGame(Func 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 000000000000..f4e0838be12a
--- /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 000000000000..f3a4f102103e
--- /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 000000000000..9b090591bcde
--- /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 000000000000..c9adc030c1b0
--- /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 bd34eaff6333..8eb9452736da 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -8,6 +8,7 @@
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;
@@ -53,10 +54,8 @@ public void Setup() => Schedule(() =>
BeatDivisor.Value = 1;
- composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
+ 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 });
});
@@ -64,7 +63,7 @@ public void Setup() => Schedule(() =>
[TestCase(2)]
public void TestSliderMultiplier(float multiplier)
{
- AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = multiplier);
+ AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier);
assertSnapDistance(100 * multiplier);
}
@@ -73,13 +72,13 @@ public void TestSliderMultiplier(float multiplier)
[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)]
@@ -97,7 +96,7 @@ public void TestConvertDurationToDistance()
assertDurationToDistance(500, 50);
assertDurationToDistance(1000, 100);
- AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
+ AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2);
assertDurationToDistance(500, 100);
assertDurationToDistance(1000, 200);
@@ -118,7 +117,7 @@ public void TestConvertDistanceToDuration()
assertDistanceToDuration(50, 500);
assertDistanceToDuration(100, 1000);
- AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
+ AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2);
assertDistanceToDuration(100, 500);
assertDistanceToDuration(200, 1000);
@@ -143,7 +142,7 @@ public void TestGetSnappedDurationFromDistance()
assertSnappedDuration(200, 2000);
assertSnappedDuration(250, 3000);
- AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
+ AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2);
assertSnappedDuration(0, 0);
assertSnappedDuration(50, 0);
@@ -175,7 +174,7 @@ public void GetSnappedDistanceFromDistance()
assertSnappedDistance(200, 200);
assertSnappedDistance(250, 200);
- AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
+ AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2);
assertSnappedDistance(50, 0);
assertSnappedDistance(100, 0);
@@ -197,20 +196,20 @@ public void GetSnappedDistanceFromDistance()
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/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
index de740dab61c1..c5a58af603e8 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
@@ -165,7 +165,7 @@ public void TestBonusObjectsExcludedFromDrain()
{
var beatmap = new Beatmap
{
- BeatmapInfo = { BaseDifficulty = { DrainRate = 10 } },
+ Difficulty = { DrainRate = 10 }
};
beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 0 });
@@ -200,7 +200,7 @@ private Beatmap createBeatmap(double startTime, double endTime, params BreakPeri
{
var beatmap = new Beatmap
{
- BeatmapInfo = { BaseDifficulty = { DrainRate = 10 } },
+ Difficulty = { DrainRate = 10 }
};
for (double time = startTime; time <= endTime; time += 100)
diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs
index e888f51e9863..dbeb453d4dde 100644
--- a/osu.Game.Tests/ImportTest.cs
+++ b/osu.Game.Tests/ImportTest.cs
@@ -17,7 +17,7 @@ public abstract class ImportTest
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 fabb016d5fb0..cfda4f6422f5 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -46,7 +46,7 @@ public void TestAddRedundantTiming()
[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 @@ public void TestAddRedundantDifficulty()
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 @@ public void TestRemoveGroupAlsoRemovedControlPoints()
[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 @@ public void TestAddControlPointToGroup()
[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 @@ public void TestRemoveControlPointFromGroup()
[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 @@ public void TestOrdering()
[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 5f76bf8bd6fe..978767c265c1 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -168,14 +168,14 @@ protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage
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 000000000000..878bf23ceab9
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 000000000000..87c7de4b7beb
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 000000000000..003fe23dca22
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 000000000000..5d380ab50cc0
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 000000000000..7cdd1939e95f
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 000000000000..795483c096cf
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 ab470674117a..ffb3d41d188d 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
@@ -6,7 +6,6 @@
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 @@ public TestSkinProvidingContainer(IEnumerable sources)
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
new file mode 100644
index 000000000000..99be72e95865
--- /dev/null
+++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
@@ -0,0 +1,173 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using ManagedBass.Fx;
+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;
+using osu.Framework.Testing;
+using osu.Game.Audio.Effects;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Tests.Visual.Audio
+{
+ public class TestSceneAudioFilter : OsuTestScene
+ {
+ private OsuSpriteText lowPassText;
+ private AudioFilter lowPassFilter;
+
+ private OsuSpriteText highPassText;
+ private AudioFilter highPassFilter;
+
+ private Track track;
+
+ private WaveformTestBeatmap beatmap;
+
+ private OsuSliderBar lowPassSlider;
+ private OsuSliderBar highPassSlider;
+
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio)
+ {
+ beatmap = new WaveformTestBeatmap(audio);
+ track = beatmap.LoadTrack();
+
+ Add(new FillFlowContainer
+ {
+ Children = new Drawable[]
+ {
+ lowPassFilter = new AudioFilter(audio.TrackMixer),
+ highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
+ lowPassText = new OsuSpriteText
+ {
+ Padding = new MarginPadding(20),
+ Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
+ Font = new FontUsage(size: 40)
+ },
+ lowPassSlider = new OsuSliderBar
+ {
+ Width = 500,
+ Height = 50,
+ Padding = new MarginPadding(20),
+ Current = new BindableInt
+ {
+ MinValue = 0,
+ MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
+ }
+ },
+ highPassText = new OsuSpriteText
+ {
+ Padding = new MarginPadding(20),
+ Text = $"High Pass: {highPassFilter.Cutoff}hz",
+ Font = new FontUsage(size: 40)
+ },
+ highPassSlider = new OsuSliderBar
+ {
+ Width = 500,
+ Height = 50,
+ Padding = new MarginPadding(20),
+ Current = new BindableInt
+ {
+ MinValue = 0,
+ MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
+ }
+ }
+ }
+ });
+
+ 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 TestLowPassSweep()
+ {
+ AddStep("Filter Sweep", () =>
+ {
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
+ .CutoffTo(0, 2000, Easing.OutCubic);
+ });
+
+ waitTrackPlay();
+
+ AddStep("Filter Sweep (reverse)", () =>
+ {
+ lowPassFilter.CutoffTo(0).Then()
+ .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
+ });
+
+ waitTrackPlay();
+ AddStep("Stop track", () => track.Stop());
+ }
+
+ [Test]
+ public void TestHighPassSweep()
+ {
+ AddStep("Filter Sweep", () =>
+ {
+ highPassFilter.CutoffTo(0).Then()
+ .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
+ });
+
+ waitTrackPlay();
+
+ AddStep("Filter Sweep (reverse)", () =>
+ {
+ highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
+ .CutoffTo(0, 2000, Easing.OutCubic);
+ });
+
+ waitTrackPlay();
+
+ AddStep("Stop track", () => track.Stop());
+ }
+
+ 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 11830ebe35a2..d1efd22d6f40 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.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 @@ private class TestDistanceSnapGrid : DistanceSnapGrid
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 @@ public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosi
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 2258a209e207..ab2bc4649a41 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -8,6 +8,7 @@
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 @@ public void TestNewBeatmapSaveThenLoad()
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 @@ public void TestNewBeatmapSaveThenLoad()
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/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
index 5abb1fb011cc..3a7fdb1e7b24 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
@@ -182,7 +182,7 @@ public void TestSliderMultiplierDoesNotAffectRelativeBeatLength()
{
var beatmap = createBeatmap();
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
- beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
+ beatmap.Difficulty.SliderMultiplier = 2;
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 5000);
@@ -196,7 +196,7 @@ public void TestSliderMultiplierAffectsNonRelativeBeatLength()
{
var beatmap = createBeatmap();
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
- beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
+ beatmap.Difficulty.SliderMultiplier = 2;
createTest(beatmap);
AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 2000);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
index 5eb71e92c2d9..ae0decaee1b0 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
@@ -103,6 +103,30 @@ public void TestInitialSeek()
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 @@ private void createStabilityContainer(double gameplayStartTime = double.MinValue
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/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
index 6e7c9c6bef01..b9fd35bba97a 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
@@ -55,7 +55,7 @@ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
{
- BeatmapInfo = { BaseDifficulty = { ApproachRate = 9 } },
+ Difficulty = { ApproachRate = 9 },
};
for (int i = 0; i < 15; i++)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 1dcd37544021..57d2e6c828ba 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -205,7 +205,7 @@ public void TestModReinstantiation()
AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen());
- AddStep("retrieve mods", () => playerMod1 = (TestMod)player.Mods.Value.Single());
+ AddStep("retrieve mods", () => playerMod1 = (TestMod)player.GameplayState.Mods.Single());
AddAssert("game mods not applied", () => gameMod.Applied == false);
AddAssert("player mods applied", () => playerMod1.Applied);
@@ -217,7 +217,7 @@ public void TestModReinstantiation()
});
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen());
- AddStep("retrieve mods", () => playerMod2 = (TestMod)player.Mods.Value.Single());
+ AddStep("retrieve mods", () => playerMod2 = (TestMod)player.GameplayState.Mods.Single());
AddAssert("game mods not applied", () => gameMod.Applied == false);
AddAssert("player has different mods", () => playerMod1 != playerMod2);
AddAssert("player mods applied", () => playerMod2.Applied);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 2f15e549f774..283fe594eabf 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -93,9 +93,9 @@ private void setUpHitObjects() => AddStep("set up hit objects", () =>
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/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
index 8fa842ec8133..4cd40d7c5336 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
@@ -90,8 +90,12 @@ public void TestOutroEndsDuringFailAnimation()
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/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
index f71d13ed3534..bfea97410ad5 100644
--- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
+++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
@@ -5,7 +5,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Screens;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osuTK;
@@ -21,6 +20,8 @@ public abstract class IntroTestScene : OsuTestScene
protected OsuScreenStack IntroStack;
+ private IntroScreen intro;
+
protected IntroTestScene()
{
Children = new Drawable[]
@@ -39,7 +40,11 @@ protected IntroTestScene()
Position = new Vector2(0.5f),
}
};
+ }
+ [Test]
+ public virtual void TestPlayIntro()
+ {
AddStep("restart sequence", () =>
{
logo.FinishTransforms();
@@ -52,12 +57,12 @@ protected IntroTestScene()
RelativeSizeAxes = Axes.Both,
});
- IntroStack.Push(CreateScreen());
+ IntroStack.Push(intro = CreateScreen());
});
- AddUntilStep("wait for menu", () => IntroStack.CurrentScreen is MainMenu);
+ AddUntilStep("wait for menu", () => intro.DidLoadMenu);
}
- protected abstract IScreen CreateScreen();
+ protected abstract IntroScreen CreateScreen();
}
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs
index 107734cc8d4f..ffc99185fbf5 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
-using osu.Framework.Screens;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
@@ -10,6 +9,6 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public class TestSceneIntroCircles : IntroTestScene
{
- protected override IScreen CreateScreen() => new IntroCircles();
+ protected override IntroScreen CreateScreen() => new IntroCircles();
}
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs
index df79584167ca..8f01e0321b9d 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
-using osu.Framework.Screens;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
@@ -10,6 +9,6 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public class TestSceneIntroTriangles : IntroTestScene
{
- protected override IScreen CreateScreen() => new IntroTriangles();
+ protected override IntroScreen CreateScreen() => new IntroTriangles();
}
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs
index 5f135febf47a..9081be3dd653 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
-using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Screens.Menu;
@@ -11,10 +10,12 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public class TestSceneIntroWelcome : IntroTestScene
{
- protected override IScreen CreateScreen() => new IntroWelcome();
+ protected override IntroScreen CreateScreen() => new IntroWelcome();
- public TestSceneIntroWelcome()
+ public override void TestPlayIntro()
{
+ base.TestPlayIntro();
+
AddUntilStep("wait for load", () => MusicController.TrackLoaded);
AddAssert("correct track", () => Precision.AlmostEquals(MusicController.CurrentTrack.Length, 48000, 1));
AddAssert("check if menu music loops", () => MusicController.CurrentTrack.Looping);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 0b707038700a..2bb77395eff0 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -564,11 +564,18 @@ public void TestGameplayFlow()
}
});
- 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 @@ public void TestGameplayFlow()
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/Mvis/TestSceneTextEditIndicator.cs b/osu.Game.Tests/Visual/Mvis/TestSceneTextEditIndicator.cs
deleted file mode 100644
index 08a4eb280bd0..000000000000
--- a/osu.Game.Tests/Visual/Mvis/TestSceneTextEditIndicator.cs
+++ /dev/null
@@ -1,51 +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.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Colour;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Utils;
-using osu.Game.Graphics;
-
-namespace osu.Game.Tests.Visual.Mvis
-{
- public class TestSceneTextEditIndicator : ScreenTestScene
- {
- private DependencyContainer dependencies;
- private TextEditIndicator indicator;
-
- protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
- dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Children = new Drawable[]
- {
- new Box
- {
- Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#777"), Color4Extensions.FromHex("#aaa")),
- RelativeSizeAxes = Axes.Both
- },
- indicator = new TextEditIndicator()
- };
-
- AddStep("Toggle Indicator", indicator.ToggleVisibility);
- AddStep("Clear Indicator", () => indicator.Text = string.Empty);
- AddStep("Random Text", () =>
- {
- indicator.Text += RNG.Next().ToString();
- indicator.Show();
- });
- AddStep("Flash", indicator.Flash);
- AddStep("Test", () =>
- {
- indicator.Text = "测试";
- indicator.Text = string.Empty;
- this.Delay(3000).Schedule(() => indicator.Text = "Hey!");
- });
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
index 7327d4053a06..2706ff5cebf7 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
@@ -7,11 +7,8 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.Platform;
-using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -29,12 +26,11 @@
using osu.Game.Screens.Menu;
using osu.Game.Skinning;
using osu.Game.Utils;
-using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Navigation
{
[TestFixture]
- public class TestSceneOsuGame : OsuTestScene
+ public class TestSceneOsuGame : OsuGameTestScene
{
private IReadOnlyList requiredGameDependencies => new[]
{
@@ -84,34 +80,12 @@ public class TestSceneOsuGame : OsuTestScene
typeof(PreviewTrackManager),
};
- private OsuGame game;
-
[Resolved]
private OsuGameBase gameBase { get; set; }
[Resolved]
private GameHost host { get; set; }
- [SetUpSteps]
- public void SetUpSteps()
- {
- AddStep("create game", () =>
- {
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black,
- },
- };
-
- AddGame(game = new OsuGame());
- });
-
- AddUntilStep("wait for load", () => game.IsLoaded);
- }
-
[Test]
public void TestNullRulesetHandled()
{
@@ -127,8 +101,8 @@ public void TestNullRulesetHandled()
[Test]
public void TestSwitchThreadExecutionMode()
{
- AddStep("Change thread mode to multi threaded", () => { game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.MultiThreaded); });
- AddStep("Change thread mode to single thread", () => { game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.SingleThread); });
+ AddStep("Change thread mode to multi threaded", () => { Game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.MultiThreaded); });
+ AddStep("Change thread mode to single thread", () => { Game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.SingleThread); });
}
[Test]
@@ -154,7 +128,7 @@ public void TestAvailableDependencies()
{
foreach (var type in requiredGameDependencies)
{
- if (game.Dependencies.Get(type) == null)
+ if (Game.Dependencies.Get(type) == null)
throw new InvalidOperationException($"{type} has not been cached");
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index aeb800f58a53..ce437e729958 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -350,13 +350,13 @@ public void TestOverlayClosing()
// since most overlays use a scroll container that absorbs on mouse down
NowPlayingOverlay nowPlayingOverlay = null;
+ AddUntilStep("Wait for now playing load", () => (nowPlayingOverlay = Game.ChildrenOfType().FirstOrDefault()) != null);
+
AddStep("enter menu", () => InputManager.Key(Key.Enter));
+ AddUntilStep("toolbar displayed", () => Game.Toolbar.State.Value == Visibility.Visible);
- AddStep("get and press now playing hotkey", () =>
- {
- nowPlayingOverlay = Game.ChildrenOfType().Single();
- InputManager.Key(Key.F6);
- });
+ AddStep("open now playing", () => InputManager.Key(Key.F6));
+ AddUntilStep("now playing is visible", () => nowPlayingOverlay.State.Value == Visibility.Visible);
// drag tests
@@ -417,7 +417,7 @@ public void TestExitGameFromSongSelect()
pushEscape(); // returns to osu! logo
AddStep("Hold escape", () => InputManager.PressKey(Key.Escape));
- AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroTriangles);
+ AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroScreen);
AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape));
AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null);
AddStep("test dispose doesn't crash", () => Game.Dispose());
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
new file mode 100644
index 000000000000..bd723eeed650
--- /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/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index 4bcc887b9f95..d948aebbbf30 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -32,12 +32,14 @@ public class TestScenePlaylistsResultsScreen : ScreenTestScene
private TestResultsScreen resultsScreen;
private int currentScoreId;
private bool requestComplete;
+ private int totalCount;
[SetUp]
public void Setup() => Schedule(() =>
{
currentScoreId = 0;
requestComplete = false;
+ totalCount = 0;
bindHandler();
});
@@ -53,7 +55,6 @@ public void TestShowWithUserScore()
});
createResults(() => userScore);
- waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
}
@@ -62,7 +63,6 @@ public void TestShowWithUserScore()
public void TestShowNullUserScore()
{
createResults();
- waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
@@ -79,7 +79,6 @@ public void TestShowUserScoreWithDelay()
});
createResults(() => userScore);
- waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1);
AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
@@ -91,7 +90,6 @@ public void TestShowNullUserScoreWithDelay()
AddStep("bind delayed handler", () => bindHandler(true));
createResults();
- waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
@@ -100,7 +98,6 @@ public void TestShowNullUserScoreWithDelay()
public void TestFetchWhenScrolledToTheRight()
{
createResults();
- waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true));
@@ -131,7 +128,6 @@ public void TestFetchWhenScrolledToTheLeft()
});
createResults(() => userScore);
- waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true));
@@ -161,13 +157,15 @@ private void createResults(Func getScore = null)
}));
});
- AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true);
+ waitForDisplay();
}
private void waitForDisplay()
{
- AddUntilStep("wait for request to complete", () => requestComplete);
- AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true);
+ AddUntilStep("wait for load to complete", () =>
+ requestComplete
+ && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
+ && resultsScreen.ScorePanelList.AllPanelsVisible);
AddWaitStep("wait for display", 5);
}
@@ -203,6 +201,7 @@ private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool
triggerFail(s);
else
triggerSuccess(s, createUserResponse(userScore));
+
break;
case IndexPlaylistScoresRequest i:
@@ -248,6 +247,8 @@ private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore)
}
};
+ totalCount++;
+
for (int i = 1; i <= scores_per_result; i++)
{
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
@@ -285,6 +286,8 @@ private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore)
},
Statistics = userScore.Statistics
});
+
+ totalCount += 2;
}
addCursor(multiplayerUserScore.ScoresAround.Lower);
@@ -325,6 +328,8 @@ private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest
{ HitResult.Great, 300 }
}
});
+
+ totalCount++;
}
addCursor(result);
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs
index d8ec89a94e8e..deb9a22184b7 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs
@@ -137,11 +137,11 @@ public void TestBeatmapUpdatedOnReImport()
InputManager.Click(MouseButton.Left);
});
- AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize == 1);
+ AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize == 1);
AddStep("re-import original beatmap", () => manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait());
- AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize != 1);
+ AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize != 1);
}
private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
index e3dae9c27eeb..d530e1f796d1 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
@@ -53,7 +53,11 @@ private class TestTargetClass
};
[SettingSource("Sample string", "Change something for a mod")]
- public Bindable StringBindable { get; } = new Bindable();
+ public Bindable StringBindable { get; } = new Bindable
+ {
+ Default = string.Empty,
+ Value = "Sample text"
+ };
}
private enum TestEnum
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index 997eac709d2d..dc5b0e0d7731 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 @@ public class TestSceneTabletSettings : OsuTestScene
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 a5b90e665500..0ae4e0c5dc55 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
@@ -205,7 +205,7 @@ private void assertCollectionHeaderDisplays(string collectionName, bool shouldDi
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 319ce5241d10..08420ec78cd9 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -142,6 +142,8 @@ public void TestChangeBeatmapViaMouseBeforeEnter()
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 @@ public void TestDifficultyIconSelecting()
});
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 @@ public void TestGroupedDifficultyIconSelecting()
});
FilterableGroupedDifficultyIcon groupIcon = null;
- AddStep("Find group icon for different ruleset", () =>
+ AddUntilStep("Find group icon for different ruleset", () =>
{
- groupIcon = set.ChildrenOfType()
- .First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3);
+ return (groupIcon = set.ChildrenOfType()
+ .FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3)) != null;
});
AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index daae6ec0a265..1787d6c28a7e 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -163,7 +163,6 @@ public void TestDeleteViaRightClick()
});
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
-
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
}
@@ -171,6 +170,7 @@ public void TestDeleteViaRightClick()
public void TestDeleteViaDatabase()
{
AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
+ AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
index 546e905dede9..8d1572769ff9 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
@@ -36,9 +36,9 @@ public void TestIncrementMultiplier()
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod));
- var mutlipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
- AddStep(@"Add multiple Mods", () => changeMods(mutlipleIncrementMods));
- AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(mutlipleIncrementMods));
+ var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
+ AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
+ AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods));
}
[Test]
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
index 393420e700c7..1b7f65f9a035 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 @@ private void createSliderBar(bool hasDescription = false)
{
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 000000000000..9ccfba7c7430
--- /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 000000000000..fb04c5bad093
--- /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 b14684200f6b..319a768e6590 100644
--- a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs
@@ -14,7 +14,7 @@ public abstract class TournamentHostTest
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.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs
index 5782301a6524..2237e389d705 100644
--- a/osu.Game.Tournament/Components/DateTextBox.cs
+++ b/osu.Game.Tournament/Components/DateTextBox.cs
@@ -26,7 +26,7 @@ public class DateTextBox : SettingsTextBox
public DateTextBox()
{
- base.Current = new Bindable();
+ base.Current = new Bindable(string.Empty);
((OsuTextBox)Control).OnCommit += (sender, newText) =>
{
diff --git a/osu.Game.Tournament/Models/SeedingResult.cs b/osu.Game.Tournament/Models/SeedingResult.cs
index 87aaf8bf3659..d37c967762ac 100644
--- a/osu.Game.Tournament/Models/SeedingResult.cs
+++ b/osu.Game.Tournament/Models/SeedingResult.cs
@@ -10,7 +10,7 @@ public class SeedingResult
{
public List Beatmaps = new List();
- public Bindable Mod = new Bindable();
+ public Bindable Mod = new Bindable(string.Empty);
public Bindable Seed = new BindableInt
{
diff --git a/osu.Game.Tournament/Models/TournamentRound.cs b/osu.Game.Tournament/Models/TournamentRound.cs
index 08b3143be160..ab39605d0748 100644
--- a/osu.Game.Tournament/Models/TournamentRound.cs
+++ b/osu.Game.Tournament/Models/TournamentRound.cs
@@ -14,8 +14,8 @@ namespace osu.Game.Tournament.Models
[Serializable]
public class TournamentRound
{
- public readonly Bindable Name = new Bindable();
- public readonly Bindable Description = new Bindable();
+ public readonly Bindable Name = new Bindable(string.Empty);
+ public readonly Bindable Description = new Bindable(string.Empty);
public readonly BindableInt BestOf = new BindableInt(9) { Default = 9, MinValue = 3, MaxValue = 23 };
diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
index c757d5e27e16..dc99a0586164 100644
--- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
@@ -149,7 +149,7 @@ public class RoundBeatmapRow : CompositeDrawable
private readonly Bindable beatmapId = new Bindable();
- private readonly Bindable mods = new Bindable();
+ private readonly Bindable mods = new Bindable(string.Empty);
private readonly Container drawableContainer;
diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
index a51348b35779..6eff554c7ca3 100644
--- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
@@ -149,7 +149,7 @@ public class SeedingBeatmapRow : CompositeDrawable
private readonly Bindable beatmapId = new Bindable();
- private readonly Bindable score = new Bindable();
+ private readonly Bindable score = new Bindable(string.Empty);
private readonly Container drawableContainer;
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
index ab53132fd29a..2d5a20e10ec7 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
@@ -127,7 +127,7 @@ protected override void UpdateAfterChildren()
score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth);
}
- private class MatchScoreCounter : ScoreCounter
+ private class MatchScoreCounter : CommaSeparatedScoreCounter
{
private OsuSpriteText displayedSpriteText;
diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs
new file mode 100644
index 000000000000..d2a39e9db7fe
--- /dev/null
+++ b/osu.Game/Audio/Effects/AudioFilter.cs
@@ -0,0 +1,145 @@
+// 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 ManagedBass.Fx;
+using osu.Framework.Audio.Mixing;
+using osu.Framework.Graphics;
+
+namespace osu.Game.Audio.Effects
+{
+ public class AudioFilter : Component, ITransformableFilter
+ {
+ ///
+ /// The maximum cutoff frequency that can be used with a low-pass filter.
+ /// This is equal to nyquist - 1hz.
+ ///
+ public const int MAX_LOWPASS_CUTOFF = 22049; // nyquist - 1hz
+
+ private readonly AudioMixer mixer;
+ private readonly BQFParameters filter;
+ private readonly BQFType type;
+
+ private bool isAttached;
+
+ private int cutoff;
+
+ ///
+ /// The cutoff frequency of this filter.
+ ///
+ public int Cutoff
+ {
+ get => cutoff;
+ set
+ {
+ if (value == cutoff)
+ return;
+
+ cutoff = value;
+ updateFilter(cutoff);
+ }
+ }
+
+ ///
+ /// A Component that implements a BASS FX BiQuad Filter Effect.
+ ///
+ /// The mixer this effect should be applied to.
+ /// The type of filter (e.g. LowPass, HighPass, etc)
+ public AudioFilter(AudioMixer mixer, BQFType type = BQFType.LowPass)
+ {
+ this.mixer = mixer;
+ this.type = type;
+
+ filter = new BQFParameters
+ {
+ lFilter = type,
+ fBandwidth = 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
+ };
+
+ Cutoff = getInitialCutoff(type);
+ }
+
+ private int getInitialCutoff(BQFType type)
+ {
+ switch (type)
+ {
+ case BQFType.HighPass:
+ return 1;
+
+ 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;
+ }
+
+ ensureAttached();
+
+ var filterIndex = mixer.Effects.IndexOf(filter);
+
+ if (filterIndex < 0) return;
+
+ if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
+ {
+ 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);
+ ensureDetached();
+ }
+ }
+}
diff --git a/osu.Game/Audio/Effects/ITransformableFilter.cs b/osu.Game/Audio/Effects/ITransformableFilter.cs
new file mode 100644
index 000000000000..fb6a924f6829
--- /dev/null
+++ b/osu.Game/Audio/Effects/ITransformableFilter.cs
@@ -0,0 +1,53 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Transforms;
+
+namespace osu.Game.Audio.Effects
+{
+ public interface ITransformableFilter
+ {
+ ///
+ /// The filter cutoff.
+ ///
+ int Cutoff { get; set; }
+ }
+
+ public static class FilterableAudioComponentExtensions
+ {
+ ///
+ /// Smoothly adjusts filter cutoff over time.
+ ///
+ /// A to which further transforms can be added.
+ public static TransformSequence CutoffTo(this T component, int newCutoff, double duration = 0, Easing easing = Easing.None)
+ where T : class, ITransformableFilter, IDrawable
+ => component.CutoffTo(newCutoff, duration, new DefaultEasingFunction(easing));
+
+ ///
+ /// Smoothly adjusts filter cutoff over time.
+ ///
+ /// A to which further transforms can be added.
+ public static TransformSequence CutoffTo(this TransformSequence sequence, int newCutoff, double duration = 0, Easing easing = Easing.None)
+ where T : class, ITransformableFilter, IDrawable
+ => sequence.CutoffTo(newCutoff, duration, new DefaultEasingFunction(easing));
+
+ ///
+ /// Smoothly adjusts filter cutoff over time.
+ ///
+ /// A to which further transforms can be added.
+ public static TransformSequence CutoffTo(this T component, int newCutoff, double duration, TEasing easing)
+ where T : class, ITransformableFilter, IDrawable
+ where TEasing : IEasingFunction
+ => component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing);
+
+ ///
+ /// Smoothly adjusts filter cutoff over time.
+ ///
+ /// A to which further transforms can be added.
+ 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.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing));
+ }
+}
diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs
index e5b6a4bc44f2..b2211e26cfff 100755
--- a/osu.Game/Beatmaps/Beatmap.cs
+++ b/osu.Game/Beatmaps/Beatmap.cs
@@ -18,17 +18,48 @@ namespace osu.Game.Beatmaps
public class Beatmap : IBeatmap
where T : HitObject
{
- public BeatmapInfo BeatmapInfo { get; set; } = new BeatmapInfo
+ private BeatmapDifficulty difficulty = new BeatmapDifficulty();
+
+ public BeatmapDifficulty Difficulty
+ {
+ get => difficulty;
+ set
+ {
+ difficulty = value;
+
+ if (beatmapInfo != null)
+ beatmapInfo.BaseDifficulty = difficulty.Clone();
+ }
+ }
+
+ private BeatmapInfo beatmapInfo;
+
+ public BeatmapInfo BeatmapInfo
{
- Metadata = new BeatmapMetadata
+ get => beatmapInfo;
+ set
{
- Artist = @"Unknown",
- Title = @"Unknown",
- AuthorString = @"Unknown Creator",
- },
- Version = @"Normal",
- BaseDifficulty = new BeatmapDifficulty()
- };
+ beatmapInfo = value;
+
+ if (beatmapInfo?.BaseDifficulty != null)
+ Difficulty = beatmapInfo.BaseDifficulty.Clone();
+ }
+ }
+
+ public Beatmap()
+ {
+ beatmapInfo = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Artist = @"Unknown",
+ Title = @"Unknown",
+ AuthorString = @"Unknown Creator",
+ },
+ Version = @"Normal",
+ BaseDifficulty = Difficulty,
+ };
+ }
[JsonIgnore]
public BeatmapMetadata Metadata => BeatmapInfo?.Metadata ?? BeatmapInfo?.BeatmapSet?.Metadata;
diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs
index f3434c515396..627e54c803df 100755
--- a/osu.Game/Beatmaps/BeatmapConverter.cs
+++ b/osu.Game/Beatmaps/BeatmapConverter.cs
@@ -40,7 +40,13 @@ protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
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/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs
index 2bb0787b4c2c..dfd21469fa1f 100755
--- a/osu.Game/Beatmaps/BeatmapDifficulty.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficulty.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;
using osu.Game.Database;
namespace osu.Game.Beatmaps
@@ -43,31 +44,31 @@ public float ApproachRate
///
public BeatmapDifficulty Clone()
{
- var diff = new BeatmapDifficulty();
+ var diff = (BeatmapDifficulty)Activator.CreateInstance(GetType());
CopyTo(diff);
return diff;
}
- public void CopyFrom(IBeatmapDifficultyInfo difficulty)
+ public virtual void CopyFrom(IBeatmapDifficultyInfo other)
{
- ApproachRate = difficulty.ApproachRate;
- DrainRate = difficulty.DrainRate;
- CircleSize = difficulty.CircleSize;
- OverallDifficulty = difficulty.OverallDifficulty;
+ ApproachRate = other.ApproachRate;
+ DrainRate = other.DrainRate;
+ CircleSize = other.CircleSize;
+ OverallDifficulty = other.OverallDifficulty;
- SliderMultiplier = difficulty.SliderMultiplier;
- SliderTickRate = difficulty.SliderTickRate;
+ SliderMultiplier = other.SliderMultiplier;
+ SliderTickRate = other.SliderTickRate;
}
- public void CopyTo(BeatmapDifficulty difficulty)
+ public virtual void CopyTo(BeatmapDifficulty other)
{
- difficulty.ApproachRate = ApproachRate;
- difficulty.DrainRate = DrainRate;
- difficulty.CircleSize = CircleSize;
- difficulty.OverallDifficulty = OverallDifficulty;
+ other.ApproachRate = ApproachRate;
+ other.DrainRate = DrainRate;
+ other.CircleSize = CircleSize;
+ other.OverallDifficulty = OverallDifficulty;
- difficulty.SliderMultiplier = SliderMultiplier;
- difficulty.SliderTickRate = SliderTickRate;
+ other.SliderMultiplier = SliderMultiplier;
+ other.SliderTickRate = SliderTickRate;
}
}
}
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index c0bdccdcabdc..99f45b5da465 100755
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -54,7 +54,7 @@ public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, R
}
}
- 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 @@ public Action PostNotification
}
}
- ///
- /// 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 @@ public void Dispose()
}
#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 92149e8b36b5..f4cec3bfe930 100644
--- a/osu.Game/Beatmaps/BeatmapModelDownloader.cs
+++ b/osu.Game/Beatmaps/BeatmapModelDownloader.cs
@@ -13,7 +13,7 @@ public class BeatmapModelDownloader : ModelDownloader
protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize, bool useSayobot, bool noVideo) =>
new DownloadBeatmapSetRequest(set, useSayobot, noVideo, 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 787559899ada..76019a15aed2 100644
--- a/osu.Game/Beatmaps/BeatmapModelManager.cs
+++ b/osu.Game/Beatmaps/BeatmapModelManager.cs
@@ -192,6 +192,13 @@ public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin
{
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,6 +209,7 @@ public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin
using (ContextFactory.GetForWrite())
{
beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == beatmapInfo.ID);
+
var metadata = beatmapInfo.Metadata ?? setInfo.Metadata;
// grab the original file (or create a new one if not found).
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index efd80a9ca9ec..ab59e9e45fc4 100755
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -15,11 +15,9 @@ public abstract class ControlPoint : IComparable, IDeepCloneable
[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 @@ public ControlPoint DeepClone()
public virtual void CopyFrom(ControlPoint other)
{
+ Time = other.Time;
}
}
}
\ No newline at end of file
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index 3ff40fe194c3..9d738ecbfb2c 100755
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -33,14 +33,6 @@ public class ControlPointInfo : IDeepCloneable
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 @@ public class ControlPointInfo : IDeepCloneable
[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 @@ public virtual void Clear()
{
groups.Clear();
timingPoints.Clear();
- difficultyPoints.Clear();
effectPoints.Clear();
}
@@ -277,10 +260,6 @@ protected virtual bool CheckAlreadyExisting(double time, ControlPoint newPoint)
case EffectControlPoint _:
existing = EffectPointAt(time);
break;
-
- case DifficultyControlPoint _:
- existing = DifficultyPointAt(time);
- break;
}
return newPoint?.IsRedundant(existing) == true;
@@ -298,9 +277,8 @@ protected virtual void GroupItemAdded(ControlPoint controlPoint)
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 @@ protected virtual void GroupItemRemoved(ControlPoint controlPoint)
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 8a6cfaf688a7..bf7ed8e6f58e 100755
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -7,17 +7,20 @@
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 @@ public class DifficultyControlPoint : ControlPoint
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 79bc88e77374..7f550a52fca2 100755
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -12,7 +12,8 @@ public class EffectControlPoint : ControlPoint
public static readonly EffectControlPoint DEFAULT = new EffectControlPoint
{
KiaiModeBindable = { Disabled = true },
- OmitFirstBarLineBindable = { Disabled = true }
+ OmitFirstBarLineBindable = { Disabled = true },
+ ScrollSpeedBindable = { Disabled = true }
};
///
@@ -20,6 +21,26 @@ public class EffectControlPoint : ControlPoint
///
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 @@ public override bool IsRedundant(ControlPoint existing)
=> !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 4aa6a3d6e994..fb489f73b129 100755
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -8,6 +8,9 @@
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/JsonBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs
index 988968fa42b5..0d5c48f64d4f 100755
--- a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs
@@ -18,7 +18,7 @@ protected override void ParseStreamInto(LineBufferedReader stream, Beatmap outpu
stream.ReadToEnd().DeserializeInto(output);
foreach (var hitObject in output.HitObjects)
- hitObject.ApplyDefaults(output.ControlPointInfo, output.BeatmapInfo.BaseDifficulty);
+ hitObject.ApplyDefaults(output.ControlPointInfo, output.Difficulty);
}
}
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 0c8167cc50aa..c2d6e9fe84f9 100755
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -67,7 +67,7 @@ protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatm
this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList();
foreach (var hitObject in this.beatmap.HitObjects)
- hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.BeatmapInfo.BaseDifficulty);
+ hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.Difficulty);
}
protected override bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(' ') || line.StartsWith('_');
@@ -276,7 +276,7 @@ private void handleDifficulty(string line)
{
var pair = SplitKeyVal(line);
- var difficulty = beatmap.BeatmapInfo.BaseDifficulty;
+ var difficulty = beatmap.Difficulty;
switch (pair.Key)
{
@@ -384,14 +384,21 @@ private void handleTimingPoint(string line)
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
{
@@ -444,7 +451,7 @@ private void handleHitObject(string line)
if (obj != null)
{
- obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty);
+ obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
beatmap.HitObjects.Add(obj);
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index aef13b88722a..1dc270ee63ba 100755
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -141,17 +141,17 @@ private void handleDifficulty(TextWriter writer)
{
writer.WriteLine("[Difficulty]");
- writer.WriteLine(FormattableString.Invariant($"HPDrainRate: {beatmap.BeatmapInfo.BaseDifficulty.DrainRate}"));
- writer.WriteLine(FormattableString.Invariant($"CircleSize: {beatmap.BeatmapInfo.BaseDifficulty.CircleSize}"));
- writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty}"));
- writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.BeatmapInfo.BaseDifficulty.ApproachRate}"));
+ writer.WriteLine(FormattableString.Invariant($"HPDrainRate: {beatmap.Difficulty.DrainRate}"));
+ writer.WriteLine(FormattableString.Invariant($"CircleSize: {beatmap.Difficulty.CircleSize}"));
+ writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.Difficulty.OverallDifficulty}"));
+ writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.Difficulty.ApproachRate}"));
// Taiko adjusts the slider multiplier (see: LEGACY_TAIKO_VELOCITY_MULTIPLIER)
writer.WriteLine(beatmap.BeatmapInfo.RulesetID == 1
- ? FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}")
- : FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}"));
+ ? FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}")
+ : FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier}"));
- writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}"));
+ writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.Difficulty.SliderTickRate}"));
}
private void handleEvents(TextWriter writer)
@@ -170,33 +170,30 @@ private void handleControlPoints(TextWriter writer)
if (beatmap.ControlPointInfo.Groups.Count == 0)
return;
- writer.WriteLine("[TimingPoints]");
-
- if (!(beatmap.ControlPointInfo is LegacyControlPointInfo))
- {
- var legacyControlPoints = new LegacyControlPointInfo();
+ var legacyControlPoints = new LegacyControlPointInfo();
+ foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
+ legacyControlPoints.Add(point.Time, point.DeepClone());
- foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
- legacyControlPoints.Add(point.Time, point.DeepClone());
+ writer.WriteLine("[TimingPoints]");
- beatmap.ControlPointInfo = legacyControlPoints;
+ SampleControlPoint lastRelevantSamplePoint = null;
+ DifficultyControlPoint lastRelevantDifficultyPoint = null;
- SampleControlPoint lastRelevantSamplePoint = null;
+ bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0;
- // iterate over hitobjects and pull out all required sample changes
- foreach (var h in beatmap.HitObjects)
- {
- var hSamplePoint = h.SampleControlPoint;
+ // iterate over hitobjects and pull out all required sample and difficulty changes
+ extractDifficultyControlPoints(beatmap.HitObjects);
+ extractSampleControlPoints(beatmap.HitObjects);
- if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint))
- {
- legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint);
- lastRelevantSamplePoint = hSamplePoint;
- }
- }
+ // handle scroll speed, which is stored as "slider velocity" in legacy formats.
+ // this is relevant for scrolling ruleset beatmaps.
+ if (!isOsuRuleset)
+ {
+ 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 @@ private void handleControlPoints(TextWriter writer)
}
// 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 @@ void outputControlPointAt(double time, bool isTimingPoint)
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 @@ void outputControlPointAt(double time, bool isTimingPoint)
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)
@@ -285,7 +331,7 @@ private void handleHitObject(TextWriter writer, HitObject hitObject)
break;
case 3:
- int totalColumns = (int)Math.Max(1, beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
+ int totalColumns = (int)Math.Max(1, beatmap.Difficulty.CircleSize);
position.X = (int)Math.Ceiling(((IHasXPosition)hitObject).X * (512f / totalColumns));
break;
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index ea131150e5d6..0e1c23eeeffb 100755
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -181,7 +181,7 @@ public LegacyDifficultyControlPoint(double beatLength)
public LegacyDifficultyControlPoint()
{
- SpeedMultiplierBindable.Precision = double.Epsilon;
+ SliderVelocityBindable.Precision = double.Epsilon;
}
public override void CopyFrom(ControlPoint other)
diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs
index f61dd269e1d9..3f598cd1e522 100755
--- a/osu.Game/Beatmaps/IBeatmap.cs
+++ b/osu.Game/Beatmaps/IBeatmap.cs
@@ -20,6 +20,11 @@ public interface IBeatmap
///
BeatmapMetadata Metadata { get; }
+ ///
+ /// This beatmap's difficulty settings.
+ ///
+ public BeatmapDifficulty Difficulty { get; set; }
+
///
/// The control points in this beatmap.
///
diff --git a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs
index ff0ca5ebe1a7..2b0a2e7a4d4a 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 @@ public class LegacyControlPointInfo : ControlPointInfo
/// 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 @@ public class LegacyControlPointInfo : ControlPointInfo
[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);
+
+ case DifficultyControlPoint _:
+ return newPoint.IsRedundant(DifficultyPointAt(time));
- return base.CheckAlreadyExisting(time, newPoint);
+ 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;
+
+ case DifficultyControlPoint typed:
+ difficultyPoints.Add(typed);
+ return;
- base.GroupItemAdded(controlPoint);
+ 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 3317acec00a4..2fd8ba0d3d25 100755
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -119,15 +119,12 @@ public virtual IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList m is IApplicableToDifficulty))
{
- converted.BeatmapInfo = converted.BeatmapInfo.Clone();
- converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone();
-
foreach (var mod in mods.OfType())
{
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
- mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty);
+ mod.ApplyToDifficulty(converted.Difficulty);
}
}
@@ -146,7 +143,7 @@ public virtual IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList
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)
@@ -208,19 +208,27 @@ private CancellationTokenSource createCancellationTokenSource(TimeSpan? timeout)
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 ad3e890b3aa1..cf83345e2aa1 100755
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -66,8 +66,12 @@ public void Invalidate(BeatmapInfo info)
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 @@ public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo)
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
+
if (working != null)
return working;
diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs
index 20dc1dd61ff3..9372fea42d40 100644
--- a/osu.Game/Collections/ManageCollectionsDialog.cs
+++ b/osu.Game/Collections/ManageCollectionsDialog.cs
@@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Audio.Effects;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -20,6 +22,8 @@ public class ManageCollectionsDialog : OsuFocusedOverlayContainer
private const double enter_duration = 500;
private const double exit_duration = 200;
+ private AudioFilter lowPassFilter;
+
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
@@ -36,7 +40,7 @@ public ManageCollectionsDialog()
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OsuColour colours, AudioManager audio)
{
Children = new Drawable[]
{
@@ -108,7 +112,8 @@ private void load(OsuColour colours)
},
}
}
- }
+ },
+ lowPassFilter = new AudioFilter(audio.TrackMixer)
};
}
@@ -116,6 +121,7 @@ protected override void PopIn()
{
base.PopIn();
+ lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
this.FadeIn(enter_duration, Easing.OutQuint);
this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint);
}
@@ -124,6 +130,8 @@ protected override void PopOut()
{
base.PopOut();
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
+
this.FadeOut(exit_duration, Easing.OutQuint);
this.ScaleTo(0.9f, exit_duration);
diff --git a/osu.Game/Configuration/MConfigManager.cs b/osu.Game/Configuration/MConfigManager.cs
index 1753f532971a..ac3e92abc718 100644
--- a/osu.Game/Configuration/MConfigManager.cs
+++ b/osu.Game/Configuration/MConfigManager.cs
@@ -34,7 +34,6 @@ protected override void InitialiseDefaults()
SetDefault(MSetting.OptUI, true);
SetDefault(MSetting.TrianglesEnabled, true);
SetDefault(MSetting.SongSelectBgBlur, 0.2f, 0f, 1f);
- SetDefault(MSetting.AlwaysHideTextIndicator, false);
//Intro Settings
SetDefault(MSetting.IntroLoadDirectToSongSelect, false);
@@ -128,7 +127,6 @@ public enum MSetting
FadeInWindowWhenEntering,
UseSystemCursor,
PreferredFont,
- AlwaysHideTextIndicator,
MvisCurrentAudioProvider,
Gamemode,
DoNotShowDisclaimer,
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index 0fdc2dc22eca..f9e8a535551f 100755
--- 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 @@ protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFa
/// 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 @@ public Task Import(params string[] paths)
public Task Import(params ImportTask[] tasks)
{
- var notification = new ProgressNotification { State = ProgressNotificationState.Active };
+ var notification = new ImportProgressNotification();
PostNotification?.Invoke(notification);
diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs
new file mode 100644
index 000000000000..024d9f2a895e
--- /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 e94af01772ae..479f33c3b460 100644
--- a/osu.Game/Database/IModelImporter.cs
+++ b/osu.Game/Database/IModelImporter.cs
@@ -10,10 +10,10 @@
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 000000000000..2bd45d4e4243
--- /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 000000000000..aaee3e117f0d
--- /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 0ff902a8bcdf..82d51e365e2c 100644
--- a/osu.Game/Database/RealmContextFactory.cs
+++ b/osu.Game/Database/RealmContextFactory.cs
@@ -5,7 +5,6 @@
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;
@@ -18,7 +17,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;
@@ -79,10 +78,11 @@ public RealmContextFactory(Storage storage, string filename)
///
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 +92,7 @@ protected override void Update()
public Realm CreateContext()
{
- if (IsDisposed)
+ if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
try
@@ -132,12 +132,11 @@ private void onMigration(Migration migration, ulong lastSchemaVersion)
/// 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 +176,23 @@ public IDisposable BlockAllOperations()
});
}
- 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 000000000000..abb69644d655
--- /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 c5aa1399a3a0..18a926fa8c46 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 @@ public static T Detach(this T item) where T : RealmObject
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 aaad72f65ccf..017ea6ec32ff 100755
--- 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 @@ public OsuScrollbar(Direction scrollDir)
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/Mf/FontInfoLabel.cs b/osu.Game/Graphics/Mf/FontInfoLabel.cs
index bbb2352d9a71..02e92f6f46ec 100644
--- a/osu.Game/Graphics/Mf/FontInfoLabel.cs
+++ b/osu.Game/Graphics/Mf/FontInfoLabel.cs
@@ -25,7 +25,7 @@ public FontInfoLabel(Font font)
{
bg = new Box
{
- Colour = Color4Extensions.FromHex("#111"),
+ Colour = Color4Extensions.FromHex("#18171c"),
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
@@ -71,7 +71,7 @@ protected override bool OnHover(HoverEvent e)
protected override void OnHoverLost(HoverLostEvent e)
{
- bg.FadeColour(Color4Extensions.FromHex("#111"), 300);
+ bg.FadeColour(Color4Extensions.FromHex("#18171c"), 300);
base.OnHoverLost(e);
}
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index 5648ef64ab95..9313d2c82727 100755
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -225,6 +225,16 @@ public static Color4 ForegroundTextColourFor(Color4 backgroundColour)
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/TextEditIndicator.cs b/osu.Game/Graphics/TextEditIndicator.cs
deleted file mode 100644
index 96f67bfd7f89..000000000000
--- a/osu.Game/Graphics/TextEditIndicator.cs
+++ /dev/null
@@ -1,258 +0,0 @@
-using System.Text;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Configuration;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Screens.Mvis.SideBar.Settings.Items;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Graphics
-{
- public class TextEditIndicator : VisibilityContainer
- {
- private readonly OsuSpriteText spriteText;
- private readonly ProgressBar bg;
- private readonly Box flashBox;
- private readonly Bindable optUI = new Bindable();
- private readonly Bindable alwaysHide = new Bindable();
- private readonly FillFlowContainer placeHolderContainer;
- private readonly Circle bar;
-
- [Resolved(canBeNull: true)]
- private OsuGame game { get; set; }
-
- private string text;
-
- public string Text
- {
- get => text;
- set
- {
- if (value == text)
- return;
-
- text = value;
-
- //猜测: 因为文本输入在input上,因此不使用Schedule()会导致osu.Framework.Graphics.Drawable+InvalidThreadForMutationException
-
- if (string.IsNullOrEmpty(value))
- {
- if (State.Value == Visibility.Visible)
- Schedule(executeTimeoutHide);
-
- Schedule(() =>
- {
- placeHolderContainer.FadeIn(150);
- spriteText.FadeOut(150);
- });
- }
- else
- {
- Schedule(() =>
- {
- abortTimeoutHide(true);
-
- placeHolderContainer.FadeOut(150);
- spriteText.FadeIn(150);
- });
- }
-
- Schedule(() =>
- {
- spriteText.Text = value;
- bg.CurrentTime = Encoding.Default.GetBytes(Text).Length;
- });
- }
- }
-
- private void executeTimeoutHide() =>
- bar.ResizeWidthTo(0, 3000).OnComplete(_ => Hide());
-
- private void abortTimeoutHide(bool animate) =>
- bar.ResizeWidthTo(0.8f, animate ? 300 : 0, Easing.OutQuint);
-
- public TextEditIndicator()
- {
- AutoSizeAxes = Axes.Both;
- Anchor = Anchor.TopCentre;
- Origin = Anchor.TopCentre;
- Masking = true;
- CornerRadius = 5f;
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Radius = 7,
- Colour = Color4.Black.Opacity(0.1f)
- };
-
- Children = new Drawable[]
- {
- bg = new ByteLengthIndicator(false)
- {
- RelativeSizeAxes = Axes.Both,
- BackgroundColour = Color4.Black.Opacity(0.5f),
- FillColour = Color4.Aqua.Opacity(0.5f),
- EndTime = 31,
- CurrentTime = 0
- },
- flashBox = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.Gold.Opacity(0f),
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Blending = BlendingParameters.Mixture
- },
- new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- AutoSizeDuration = 300,
- AutoSizeEasing = Easing.OutQuint,
- Margin = new MarginPadding(10),
- Spacing = new Vector2(3),
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- new Container
- {
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Children = new Drawable[]
- {
- new PlaceHolder
- {
- Height = 28
- },
- placeHolderContainer = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Spacing = new Vector2(5),
- Margin = new MarginPadding { Vertical = 5 },
- Colour = Color4.White.Opacity(0.6f),
- Children = new Drawable[]
- {
- new SpriteIcon
- {
- Size = new Vector2(14),
- Icon = FontAwesome.Solid.Pen,
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Margin = new MarginPadding { Vertical = 2 }
- },
- new OsuSpriteText
- {
- Text = "暂无输入",
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft
- }
- }
- },
- spriteText = new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Margin = new MarginPadding { Vertical = 5 },
- },
- }
- },
- bar = new Circle
- {
- Height = 3,
- RelativeSizeAxes = Axes.X,
- Width = 0.8f,
- Colour = Color4.White,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre
- },
- new OsuSpriteText
- {
- Text = "若输入法完成编辑后没有出现文字,请尝试多按几次空格",
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre
- }
- }
- }
- };
- }
-
- [BackgroundDependencyLoader]
- private void load(MConfigManager config)
- {
- config.BindWith(MSetting.OptUI, optUI);
- config.BindWith(MSetting.AlwaysHideTextIndicator, alwaysHide);
-
- optUI.BindValueChanged(v =>
- {
- if (!v.NewValue) Hide();
- else if (!string.IsNullOrEmpty(Text)) Show();
- });
-
- alwaysHide.BindValueChanged(v =>
- {
- if (v.NewValue) Hide();
- });
- }
-
- protected override void UpdateAfterChildren()
- {
- Margin = new MarginPadding { Top = (game?.ToolbarOffset ?? 0) + 5 };
- base.UpdateAfterChildren();
- }
-
- public override void Show()
- {
- if (!optUI.Value || alwaysHide.Value) return;
-
- base.Show();
- }
-
- protected override void PopIn()
- {
- var emptyText = string.IsNullOrEmpty(Text);
- abortTimeoutHide(!emptyText);
-
- //在某些系统下窗口会莫名进入编辑状态,此时因为没有文本,所以不要显示
- if (emptyText) return;
-
- this.FadeIn(300, Easing.OutSine)
- .MoveToY(0, 300, Easing.OutQuint);
- }
-
- protected override void PopOut()
- {
- this.FadeOut(300, Easing.OutQuint)
- .MoveToY(-23, 300, Easing.OutQuint);
- }
-
- public void Flash() =>
- flashBox.FlashColour(Color4.Gold, 1000, Easing.OutQuint);
-
- private class ByteLengthIndicator : ProgressBar
- {
- protected override void UpdateValue(float value)
- {
- fill.ResizeWidthTo(value, 300, Easing.OutQuint);
- fill.FadeColour(CurrentNumber.Value >= CurrentNumber.MaxValue
- ? Color4.Red.Opacity(0.5f)
- : Color4.Aqua.Opacity(0.5f), 300);
- }
-
- public ByteLengthIndicator(bool allowSeek)
- : base(allowSeek)
- {
- fill.RelativeSizeAxes = Axes.Both;
- }
- }
- }
-}
diff --git a/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs
new file mode 100644
index 000000000000..4e1c612f090e
--- /dev/null
+++ b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Extensions.LocalisationExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Graphics.UserInterface
+{
+ public abstract class CommaSeparatedScoreCounter : RollingCounter
+ {
+ protected override double RollingDuration => 1000;
+ protected override Easing RollingEasing => Easing.Out;
+
+ protected override double GetProportionalDuration(double currentValue, double newValue) =>
+ currentValue > newValue ? currentValue - newValue : newValue - currentValue;
+
+ protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(@"N0");
+
+ protected override OsuSpriteText CreateSpriteText()
+ => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true));
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs
index 6807d007bb9e..8f0fed580f05 100755
--- 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.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(COLLAPSED_SIZE, 12);
-
- BorderColour = Color4.White;
- BorderThickness = border_width;
+ Size = new Vector2(EXPANDED_SIZE, HEIGHT);
- 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 @@ public bool Glowing
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 @@ public Color4 AccentColour
{
accentColour = value;
if (!Glowing)
- Colour = value;
+ main.Colour = value;
}
}
@@ -139,7 +140,7 @@ public Color4 GlowingAccentColour
{
glowingAccentColour = value;
if (Glowing)
- Colour = value;
+ main.Colour = value;
}
}
@@ -152,10 +153,22 @@ public Color4 GlowColour
{
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 5f2d884cd766..e8f80dec57e5 100755
--- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
@@ -9,16 +9,11 @@
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 @@ private void load(AudioManager audio)
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 fe88e6f78a58..5831d9ab1f44 100755
--- 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.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 @@ public Color4 AccentColour
}
}
- [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 @@ protected class OsuDropdownMenu : DropdownMenu, IHasAccentColour
{
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 @@ public OsuDropdownMenu()
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 @@ private void updateColours()
{
BackgroundColourHover = accentColour ?? nonAccentHoverColour;
BackgroundColourSelected = accentColour ?? nonAccentSelectedColour;
+ BackgroundColour = BackgroundColourHover.Opacity(0);
+
UpdateBackgroundColour();
UpdateForegroundColour();
}
@@ -178,8 +184,6 @@ public DrawableOsuDropdownMenuItem(MenuItem item)
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- BackgroundColour = Color4.Transparent;
-
nonAccentHoverColour = colours.PinkDarker;
nonAccentSelectedColour = Color4.Black.Opacity(0.5f);
updateColours();
@@ -187,16 +191,29 @@ private void load(OsuColour colours)
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 @@ public LocalisableString Text
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 @@ public virtual Color4 AccentColour
public OsuDropdownHeader()
{
- Foreground.Padding = new MarginPadding(4);
+ Foreground.Padding = new MarginPadding(10);
AutoSizeAxes = Axes.None;
Margin = new MarginPadding { Bottom = 4 };
@@ -303,8 +352,7 @@ public OsuDropdownHeader()
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 @@ public OsuDropdownHeader()
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 f85f9327fa2f..6963f7335e49 100755
--- 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.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
+using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface
{
@@ -52,34 +55,63 @@ public Color4 AccentColour
{
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 @@ public OsuSliderBar()
{
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
- Expanded = true,
+ Current = { Value = true }
},
},
new HoverClickSounds()
@@ -97,11 +129,12 @@ public OsuSliderBar()
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 @@ protected override void LoadComplete()
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/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
index 7ebf3819e4c7..25f19aa0a986 100755
--- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs
+++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
@@ -13,43 +14,30 @@ public abstract class ScoreCounter : RollingCounter
protected override double RollingDuration => 1000;
protected override Easing RollingEasing => Easing.Out;
- ///
- /// Whether comma separators should be displayed.
- ///
- public bool UseCommaSeparator { get; }
-
public Bindable RequiredDisplayDigits { get; } = new Bindable();
+ private string formatString;
+
///
/// Displays score.
///
/// How many leading zeroes the counter will have.
- /// Whether comma separators should be displayed.
- protected ScoreCounter(int leading = 0, bool useCommaSeparator = false)
+ protected ScoreCounter(int leading = 0)
{
- UseCommaSeparator = useCommaSeparator;
-
RequiredDisplayDigits.Value = leading;
- RequiredDisplayDigits.BindValueChanged(_ => UpdateDisplay());
+ RequiredDisplayDigits.BindValueChanged(displayDigitsChanged, true);
}
- protected override double GetProportionalDuration(double currentValue, double newValue)
+ private void displayDigitsChanged(ValueChangedEvent _)
{
- return currentValue > newValue ? currentValue - newValue : newValue - currentValue;
+ formatString = new string('0', RequiredDisplayDigits.Value);
+ UpdateDisplay();
}
- protected override LocalisableString FormatCount(double count)
- {
- string format = new string('0', RequiredDisplayDigits.Value);
-
- if (UseCommaSeparator)
- {
- for (int i = format.Length - 3; i > 0; i -= 3)
- format = format.Insert(i, @",");
- }
+ protected override double GetProportionalDuration(double currentValue, double newValue) =>
+ currentValue > newValue ? currentValue - newValue : newValue - currentValue;
- return ((long)count).ToString(format);
- }
+ protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(formatString);
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true));
diff --git a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs
index 965734792c6a..c01ee1a05980 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 @@ public class SlimEnumDropdown : OsuEnumDropdown
{
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 18869ad06c8a..7dbb5cc45c70 100755
--- 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 @@ public float? FixedLabelWidth
///
protected readonly T Component;
+ private readonly Box background;
private readonly GridContainer grid;
private readonly OsuTextFlowContainer labelText;
private readonly OsuTextFlowContainer descriptionText;
@@ -62,10 +66,9 @@ protected LabelledDrawable(bool padded)
InternalChildren = new Drawable[]
{
- new Box
+ background = new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex("1c2125"),
},
new FillFlowContainer
{
@@ -146,9 +149,10 @@ private void updateLabelWidth()
}
}
- [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 000000000000..23ebc6e98de4
--- /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 a7fd25b554d3..deb2e6baf66b 100755
--- 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.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
+using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@@ -66,11 +69,11 @@ public SwitchButton()
};
}
- [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 000000000000..f5709b515869
--- /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 c9c8d9c43f52..f3c577997a7e 100644
--- a/osu.Game/Localisation/AudioSettingsStrings.cs
+++ b/osu.Game/Localisation/AudioSettingsStrings.cs
@@ -24,6 +24,11 @@ public static class AudioSettingsStrings
///
public static LocalisableString VolumeHeader => new TranslatableString(getKey(@"volume_header"), @"音量");
+ ///
+ /// "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 6efb7884bc17..875f99af7331 100644
--- a/osu.Game/Localisation/GameplaySettingsStrings.cs
+++ b/osu.Game/Localisation/GameplaySettingsStrings.cs
@@ -14,11 +14,36 @@ public static class GameplaySettingsStrings
///
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 70d469ec9790..6d5e918aef81 100644
--- a/osu.Game/Localisation/GraphicsSettingsStrings.cs
+++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs
@@ -104,6 +104,11 @@ public static class GraphicsSettingsStrings
///
public static LocalisableString HitLighting => new TranslatableString(getKey(@"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 000000000000..a356c9e20bed
--- /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 1954ef53fc2f..2e3278ab7cf4 100644
--- a/osu.Game/Localisation/SkinSettingsStrings.cs
+++ b/osu.Game/Localisation/SkinSettingsStrings.cs
@@ -14,6 +14,11 @@ public static class SkinSettingsStrings
///
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 000000000000..5049c1384d3d
--- /dev/null
+++ b/osu.Game/Models/RealmBeatmap.cs
@@ -0,0 +1,117 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Rulesets;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ ///
+ /// A single beatmap difficulty.
+ ///
+ [ExcludeFromDynamicCompile]
+ [Serializable]
+ [MapTo("Beatmap")]
+ public class RealmBeatmap : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo
+ {
+ [PrimaryKey]
+ public Guid ID { get; set; } = Guid.NewGuid();
+
+ public string DifficultyName { get; set; } = string.Empty;
+
+ public RealmRuleset Ruleset { get; set; } = null!;
+
+ public RealmBeatmapDifficulty Difficulty { get; set; } = null!;
+
+ public RealmBeatmapMetadata Metadata { get; set; } = null!;
+
+ public RealmBeatmapSet? BeatmapSet { get; set; }
+
+ public BeatmapSetOnlineStatus Status
+ {
+ get => (BeatmapSetOnlineStatus)StatusInt;
+ set => StatusInt = (int)value;
+ }
+
+ [MapTo(nameof(Status))]
+ public int StatusInt { get; set; }
+
+ public int? OnlineID { get; set; }
+
+ public double Length { get; set; }
+
+ public double BPM { get; set; }
+
+ public string Hash { get; set; } = string.Empty;
+
+ public double StarRating { get; set; }
+
+ public string MD5Hash { get; set; } = string.Empty;
+
+ [JsonIgnore]
+ public bool Hidden { get; set; }
+
+ public RealmBeatmap(RealmRuleset ruleset, RealmBeatmapDifficulty difficulty, RealmBeatmapMetadata metadata)
+ {
+ Ruleset = ruleset;
+ Difficulty = difficulty;
+ Metadata = metadata;
+ }
+
+ [UsedImplicitly]
+ private RealmBeatmap()
+ {
+ }
+
+ #region Properties we may not want persisted (but also maybe no harm?)
+
+ public double AudioLeadIn { get; set; }
+
+ public float StackLeniency { get; set; } = 0.7f;
+
+ public bool SpecialStyle { get; set; }
+
+ public bool LetterboxInBreaks { get; set; }
+
+ public bool WidescreenStoryboard { get; set; }
+
+ public bool EpilepsyWarning { get; set; }
+
+ public bool SamplesMatchPlaybackRate { get; set; }
+
+ public double DistanceSpacing { get; set; }
+
+ public int BeatDivisor { get; set; }
+
+ public int GridSize { get; set; }
+
+ public double TimelineZoom { get; set; }
+
+ #endregion
+
+ public bool AudioEquals(RealmBeatmap? other) => other != null
+ && BeatmapSet != null
+ && other.BeatmapSet != null
+ && BeatmapSet.Hash == other.BeatmapSet.Hash
+ && Metadata.AudioFile == other.Metadata.AudioFile;
+
+ public bool BackgroundEquals(RealmBeatmap? other) => other != null
+ && BeatmapSet != null
+ && other.BeatmapSet != null
+ && BeatmapSet.Hash == other.BeatmapSet.Hash
+ && Metadata.BackgroundFile == other.Metadata.BackgroundFile;
+
+ IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
+ IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
+ IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
+ IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => Difficulty;
+ }
+}
diff --git a/osu.Game/Models/RealmBeatmapDifficulty.cs b/osu.Game/Models/RealmBeatmapDifficulty.cs
new file mode 100644
index 000000000000..3c1dad69e4a8
--- /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 000000000000..6ea7170d0f90
--- /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 000000000000..314ca4494b71
--- /dev/null
+++ b/osu.Game/Models/RealmBeatmapSet.cs
@@ -0,0 +1,78 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [MapTo("BeatmapSet")]
+ public class RealmBeatmapSet : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IBeatmapSetInfo
+ {
+ [PrimaryKey]
+ public Guid ID { get; set; } = Guid.NewGuid();
+
+ public int? OnlineID { get; set; }
+
+ public DateTimeOffset DateAdded { get; set; }
+
+ public IBeatmapMetadataInfo? Metadata => Beatmaps.FirstOrDefault()?.Metadata;
+
+ public IList Beatmaps { get; } = null!;
+
+ public IList Files { get; } = null!;
+
+ public bool DeletePending { get; set; }
+
+ public string Hash { get; set; } = string.Empty;
+
+ ///
+ /// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present).
+ ///
+ public bool Protected { get; set; }
+
+ public double MaxStarDifficulty => Beatmaps.Max(b => b.StarRating);
+
+ public double MaxLength => Beatmaps.Max(b => b.Length);
+
+ public double MaxBPM => Beatmaps.Max(b => b.BPM);
+
+ ///
+ /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
+ /// The path returned is relative to the user file storage.
+ ///
+ /// The name of the file to get the storage path of.
+ public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.StoragePath;
+
+ public override string ToString() => Metadata?.ToString() ?? base.ToString();
+
+ public bool Equals(RealmBeatmapSet? other)
+ {
+ if (other == null)
+ return false;
+
+ if (IsManaged && other.IsManaged)
+ return ID == other.ID;
+
+ if (OnlineID.HasValue && other.OnlineID.HasValue)
+ return OnlineID == other.OnlineID;
+
+ if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
+ return Hash == other.Hash;
+
+ return ReferenceEquals(this, other);
+ }
+
+ IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps;
+
+ IEnumerable IBeatmapSetInfo.Files => Files;
+ }
+}
diff --git a/osu.Game/Models/RealmFile.cs b/osu.Game/Models/RealmFile.cs
new file mode 100644
index 000000000000..2715f4be4562
--- /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 000000000000..ba12d51d0b33
--- /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 000000000000..0dcd701ed235
--- /dev/null
+++ b/osu.Game/Models/RealmRuleset.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using JetBrains.Annotations;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Models
+{
+ [ExcludeFromDynamicCompile]
+ [MapTo("Ruleset")]
+ public class RealmRuleset : RealmObject, IEquatable, IRulesetInfo
+ {
+ [PrimaryKey]
+ public string ShortName { get; set; } = string.Empty;
+
+ public int? OnlineID { get; set; }
+
+ public string Name { get; set; } = string.Empty;
+
+ public string InstantiationInfo { get; set; } = string.Empty;
+
+ public RealmRuleset(string shortName, string name, string instantiationInfo, int? onlineID = null)
+ {
+ ShortName = shortName;
+ Name = name;
+ InstantiationInfo = instantiationInfo;
+ OnlineID = onlineID;
+ }
+
+ [UsedImplicitly]
+ private RealmRuleset()
+ {
+ }
+
+ public RealmRuleset(int? onlineID, string name, string shortName, bool available)
+ {
+ OnlineID = onlineID;
+ Name = name;
+ ShortName = shortName;
+ Available = available;
+ }
+
+ public bool Available { get; set; }
+
+ public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo;
+
+ public override string ToString() => Name;
+
+ public RealmRuleset Clone() => new RealmRuleset
+ {
+ OnlineID = OnlineID,
+ Name = Name,
+ ShortName = ShortName,
+ InstantiationInfo = InstantiationInfo,
+ Available = Available
+ };
+ }
+}
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index 4b71ebdc3f69..4d843eed8aef 100755
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -180,6 +180,24 @@ public static LinkDetails GetLinkDetails(string url)
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;
}
}
@@ -327,7 +345,8 @@ public enum LinkAction
SearchBeatmapSet,
OpenWiki,
Custom,
- OpenPictureURL
+ OpenPictureURL,
+ OpenChangelog,
}
public class Link : IComparable
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 75bbaec0ef9e..28505f6b0e31 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -374,7 +374,7 @@ async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user)
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 5f71b4be4a71..39fc7f1da88e 100755
--- a/osu.Game/Online/Rooms/Room.cs
+++ b/osu.Game/Online/Rooms/Room.cs
@@ -130,12 +130,6 @@ private int? maxAttempts
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 @@ public void CopyFrom(Room other)
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 251fce29c70d..3097b0e90bd7 100755
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -98,6 +98,8 @@ public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUser
private WikiOverlay wikiOverlay;
+ private ChangelogOverlay changelogOverlay;
+
private SkinEditorOverlay skinEditor;
private Container overlayContent;
@@ -275,13 +277,6 @@ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnl
[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);
@@ -410,6 +405,17 @@ public void HandleLink(LinkDetails link) => Schedule(() =>
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.");
}
@@ -475,6 +481,18 @@ public void ShowChannel(string channel) => waitForReady(() => channelManager, _
/// 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.
@@ -610,6 +628,7 @@ private void beatmapChanged(ValueChangedEvent beatmap)
{
beatmap.OldValue?.CancelAsyncLoad();
beatmap.NewValue?.BeginAsyncLoad();
+ Logger.Log($"Game-wide working beatmap updated to {beatmap.NewValue}");
}
private void modsChanged(ValueChangedEvent> mods)
@@ -698,7 +717,7 @@ protected override void LoadComplete()
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);
@@ -861,7 +880,7 @@ protected override void LoadComplete()
loadComponentSingleFile(Picture = new OnlinePictureOverlay(), 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);
@@ -934,6 +953,19 @@ protected override void LoadComplete()
{
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 c0b55126df6f..8bfaed8c729f 100755
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -6,6 +6,7 @@
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@@ -190,8 +191,6 @@ private void load()
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client"));
- AddInternal(realmFactory);
-
dependencies.CacheAs(Storage);
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures")));
@@ -429,11 +428,28 @@ public void Migrate(string path)
{
Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""...");
- using (realmFactory.BlockAllOperations())
+ IDisposable realmBlocker = null;
+
+ try
{
- contextFactory.FlushConnections();
+ ManualResetEventSlim readyToRun = new ManualResetEventSlim();
+
+ Scheduler.Add(() =>
+ {
+ realmBlocker = realmFactory.BlockAllOperations();
+ contextFactory.FlushConnections();
+
+ readyToRun.Set();
+ }, false);
+
+ readyToRun.Wait();
+
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
}
+ finally
+ {
+ realmBlocker?.Dispose();
+ }
Logger.Log(@"Migration complete!");
}
@@ -530,6 +546,7 @@ protected override void Dispose(bool isDisposing)
LocalConfig?.Dispose();
contextFactory?.FlushConnections();
+ realmFactory?.Dispose();
}
}
}
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 935a89b99b58..a7c59ee4290e 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -335,5 +335,11 @@ protected override void Update()
if (shouldShowMore)
filterControl.FetchNextPage();
}
+
+ protected override void PopIn()
+ {
+ base.PopIn();
+ Schedule(filterControl.TakeFocus);
+ }
}
}
diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs
index f051e09c08d5..9db0f34d1b36 100755
--- a/osu.Game/Overlays/DialogOverlay.cs
+++ b/osu.Game/Overlays/DialogOverlay.cs
@@ -7,7 +7,10 @@
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Input.Events;
+using osu.Game.Audio.Effects;
namespace osu.Game.Overlays
{
@@ -18,6 +21,8 @@ public class DialogOverlay : OsuFocusedOverlayContainer
protected override string PopInSampleName => "UI/dialog-pop-in";
protected override string PopOutSampleName => "UI/dialog-pop-out";
+ private AudioFilter lowPassFilter;
+
public PopupDialog CurrentDialog { get; private set; }
public DialogOverlay()
@@ -34,6 +39,12 @@ public DialogOverlay()
Origin = Anchor.BottomCentre;
}
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio)
+ {
+ AddInternal(lowPassFilter = new AudioFilter(audio.TrackMixer));
+ }
+
public void Push(PopupDialog dialog)
{
if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return;
@@ -71,12 +82,15 @@ protected override void PopIn()
{
base.PopIn();
this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint);
+ lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
}
protected override void PopOut()
{
base.PopOut();
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
+
if (CurrentDialog?.State.Value == Visibility.Visible)
{
CurrentDialog.Hide();
diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs
index 3105ecd742b3..f8cd31f1934c 100755
--- a/osu.Game/Overlays/Notifications/ProgressNotification.cs
+++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs
@@ -31,10 +31,12 @@ public float Progress
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 51214fe460b5..198aa1438a67 100755
--- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs
+++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs
@@ -29,7 +29,7 @@ public class TrackedSettingToast : Toast
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 c02db40ecaee..4ca3ace8a136 100755
--- a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs
+++ b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs
@@ -14,10 +14,7 @@ public class DangerousSettingsButton : SettingsButton
[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 d697b4542415..0c54ae2763d9 100755
--- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
@@ -28,6 +28,7 @@ private void load()
{
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 000000000000..dba64d695a04
--- /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 000000000000..94e0c5e4942a
--- /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 000000000000..aaa60ce81b65
--- /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 3a0265e453d8..d4e4fd571d82 100755
--- 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 @@ 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 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 000000000000..e1b452e322fc
--- /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 000000000000..962572ca6e7d
--- /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 42d9d48d7381..120e2d908c5a 100755
--- 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 @@ public class GameplaySection : SettingsSection
public override Drawable CreateIcon() => new SpriteIcon
{
- Icon = FontAwesome.Regular.Circle
+ Icon = FontAwesome.Regular.DotCircle
};
public GameplaySection()
@@ -28,27 +23,13 @@ public GameplaySection()
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 20b1d8d8012d..dbb9ddc1c134 100755
--- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs
@@ -9,25 +9,15 @@
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 fd0718f9f20c..591848506a4d 100755
--- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
@@ -22,9 +22,9 @@ public GraphicsSection()
{
Children = new Drawable[]
{
- new RendererSettings(),
new LayoutSettings(),
- new DetailSettings(),
+ new RendererSettings(),
+ new ScreenshotSettings(),
};
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
index cf8adf27851b..da789db79a65 100755
--- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
@@ -78,7 +78,7 @@ public KeyBindingRow(object action, List bindings)
private RealmContextFactory realmFactory { get; set; }
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -101,7 +101,7 @@ private void load(OsuColour colours)
EdgeEffect = new EdgeEffectParameters
{
Radius = 2,
- Colour = colours.YellowDark.Opacity(0),
+ Colour = colourProvider.Highlight1.Opacity(0),
Type = EdgeEffectType.Shadow,
Hollow = true,
},
@@ -110,13 +110,12 @@ private void load(OsuColour colours)
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black,
- Alpha = 0.6f,
+ Colour = colourProvider.Background5,
},
text = new OsuSpriteText
{
Text = action.GetLocalisableDescription(),
- Margin = new MarginPadding(padding),
+ Margin = new MarginPadding(1.5f * padding),
},
buttons = new FillFlowContainer
{
@@ -405,7 +404,8 @@ public class KeyButton : Container
private readonly Box box;
public readonly OsuSpriteText Text;
- private Color4 hoverColour;
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
private bool isBinding;
@@ -448,7 +448,6 @@ public KeyButton(RealmKeyBinding keyBinding)
box = new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black
},
Text = new OsuSpriteText
{
@@ -463,9 +462,9 @@ public KeyButton(RealmKeyBinding keyBinding)
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load()
{
- hoverColour = colours.YellowDark;
+ updateHoverState();
}
protected override bool OnHover(HoverEvent e)
@@ -484,12 +483,12 @@ private void updateHoverState()
{
if (isBinding)
{
- box.FadeColour(Color4.White, transition_time, Easing.OutQuint);
+ box.FadeColour(colourProvider.Light2, transition_time, Easing.OutQuint);
Text.FadeColour(Color4.Black, transition_time, Easing.OutQuint);
}
else
{
- box.FadeColour(IsHovered ? hoverColour : Color4.Black, transition_time, Easing.OutQuint);
+ box.FadeColour(IsHovered ? colourProvider.Light4 : colourProvider.Background6, transition_time, Easing.OutQuint);
Text.FadeColour(IsHovered ? Color4.Black : Color4.White, transition_time, Easing.OutQuint);
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
index 2cc2857e9b6d..2051af6f3cd6 100755
--- 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.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Game.Database;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Localisation;
@@ -27,8 +26,7 @@ protected KeyBindingsSubsection(int? variant)
{
this.variant = variant;
- FlowContent.Spacing = new Vector2(0, 1);
- FlowContent.Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS };
+ FlowContent.Spacing = new Vector2(0, 3);
}
[BackgroundDependencyLoader]
@@ -60,7 +58,7 @@ private void load(RealmContextFactory realmFactory)
}
}
- 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 26610628d571..3ef5ce894176 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.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 @@ public RotationPresetButtons(ITabletHandler tabletHandler)
RelativeSizeAxes = Axes.X;
Height = height;
- for (int i = 0; i < 360; i += 90)
+ IEnumerable createColumns(int count)
+ {
+ for (int i = 0; i < count; ++i)
+ {
+ if (i > 0)
+ yield return new Dimension(GridSizeMode.Absolute, 10);
+
+ yield return new Dimension();
+ }
+ }
+
+ GridContainer grid;
+
+ InternalChild = grid = new GridContainer
{
- var presetRotation = i;
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = createColumns(preset_count).ToArray()
+ };
- Add(new RotationButton(i)
+ 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.X,
- Height = height,
- Width = 0.25f,
- Text = $@"{presetRotation}º",
- Action = () => tabletHandler.Rotation.Value = presetRotation,
- });
+ RelativeSizeAxes = Axes.Both,
+ Height = 1,
+ Text = $@"{rotationValue}º",
+ Action = () => tabletHandler.Rotation.Value = rotationValue,
+ };
+ grid.Content[0][2 * i] = rotationPresets[i] = rotationPreset;
}
}
@@ -49,16 +79,19 @@ protected override void LoadComplete()
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 @@ protected override void LoadComplete()
private void updateColour()
{
- if (isSelected)
- {
- BackgroundColour = colours.BlueDark;
- Triangles.ColourDark = colours.BlueDarker;
- Triangles.ColourLight = colours.Blue;
- }
- else
- {
- BackgroundColour = colours.Gray4;
- Triangles.ColourDark = colours.Gray5;
- Triangles.ColourLight = colours.Gray6;
- }
+ BackgroundColour = isSelected ? colours.Blue3 : colourProvider.Background3;
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 9a1251125c2f..f05dbe4e9d63 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -165,7 +165,13 @@ private void load(OsuColour colours)
LabelText = TabletSettingsStrings.Rotation,
Current = rotation
},
- new RotationPresetButtons(tabletHandler),
+ new RotationPresetButtons(tabletHandler)
+ {
+ Padding = new MarginPadding
+ {
+ Horizontal = SettingsPanel.CONTENT_MARGINS
+ }
+ },
new SettingsSlider
{
TransferValueOnCommit = true,
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
index 95bf6656bf17..62f4b82affc5 100755
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
@@ -10,7 +10,6 @@
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 @@ public class GeneralSettings : SettingsSubsection
{
protected override LocalisableString Header => "整体";
- private TriangleButton importBeatmapsButton;
- private TriangleButton importScoresButton;
- private TriangleButton importSkinsButton;
- private TriangleButton importCollectionsButton;
- private TriangleButton deleteBeatmapsButton;
- private TriangleButton deleteScoresButton;
- private TriangleButton deleteSkinsButton;
- private TriangleButton restoreButton;
- private TriangleButton undeleteButton;
+ private SettingsButton importBeatmapsButton;
+ private SettingsButton importScoresButton;
+ private SettingsButton importSkinsButton;
+ private SettingsButton importCollectionsButton;
+ private SettingsButton deleteBeatmapsButton;
+ private SettingsButton deleteScoresButton;
+ private SettingsButton deleteSkinsButton;
+ private SettingsButton restoreButton;
+ private SettingsButton undeleteButton;
[BackgroundDependencyLoader(permitNulls: true)]
private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] StableImportManager stableImportManager, DialogOverlay dialogOverlay)
diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
index fa0c06167b2c..9410a87848c1 100755
--- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
@@ -6,7 +6,6 @@
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings.Sections.Maintenance;
-using osuTK;
namespace osu.Game.Overlays.Settings.Sections
{
@@ -21,7 +20,6 @@ public class MaintenanceSection : SettingsSection
public MaintenanceSection()
{
- FlowContent.Spacing = new Vector2(0, 5);
Children = new Drawable[]
{
new GeneralSettings()
diff --git a/osu.Game/Overlays/Settings/Sections/Mf/MfSettings.cs b/osu.Game/Overlays/Settings/Sections/Mf/MfSettings.cs
index 2e946bf70e5c..1621d37dfeda 100644
--- a/osu.Game/Overlays/Settings/Sections/Mf/MfSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Mf/MfSettings.cs
@@ -27,12 +27,6 @@ private void load(MConfigManager config, OsuConfigManager osuConfig, GameHost ho
Current = config.GetBindable(MSetting.OptUI)
},
new SettingsCheckbox
- {
- LabelText = "总是隐藏输入指示器",
- TooltipText = "如果你的窗口经常无缘无故进入编辑状态,或者只是觉得弹出来烦,那么我建议勾选此项",
- Current = config.GetBindable(MSetting.AlwaysHideTextIndicator)
- },
- new SettingsCheckbox
{
LabelText = "启用三角形粒子动画",
Current = config.GetBindable(MSetting.TrianglesEnabled)
diff --git a/osu.Game/Overlays/Settings/Sections/RulesetSection.cs b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs
new file mode 100644
index 000000000000..b9339d5299f9
--- /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 d340b639494e..b770b0da7031 100755
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -16,7 +16,6 @@
using osu.Game.Localisation;
using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
-using osuTK;
namespace osu.Game.Overlays.Settings.Sections
{
@@ -63,43 +62,18 @@ private int firstNonDefaultSkinIndex
[BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor)
{
- FlowContent.Spacing = new Vector2(0, 5);
-
Children = new Drawable[]
{
- skinDropdown = new SkinSettingsDropdown(),
+ skinDropdown = new SkinSettingsDropdown
+ {
+ LabelText = SkinSettingsStrings.CurrentSkin
+ },
new SettingsButton
{
Text = SkinSettingsStrings.SkinLayoutEditor,
Action = () => skinEditor?.Toggle(),
},
new ExportSkinButton(),
- new SettingsSlider
- {
- LabelText = SkinSettingsStrings.GameplayCursorSize,
- Current = config.GetBindable(OsuSetting.GameplayCursorSize),
- KeyboardStep = 0.01f
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.AutoCursorSize,
- Current = config.GetBindable(OsuSetting.AutoCursorSize)
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.BeatmapSkins,
- Current = config.GetBindable(OsuSetting.BeatmapSkins)
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.BeatmapColours,
- Current = config.GetBindable(OsuSetting.BeatmapColours)
- },
- new SettingsCheckbox
- {
- LabelText = SkinSettingsStrings.BeatmapHitsounds,
- Current = config.GetBindable(OsuSetting.BeatmapHitsounds)
- },
};
managerUpdated = skins.ItemUpdated.GetBoundCopy();
diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs
index 4d640d5707e7..ed8e217c9a68 100755
--- a/osu.Game/Overlays/Settings/SettingsButton.cs
+++ b/osu.Game/Overlays/Settings/SettingsButton.cs
@@ -7,11 +7,11 @@
using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics;
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 167061f48573..2ed7d6063a32 100755
--- a/osu.Game/Overlays/Settings/SettingsDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs
@@ -6,6 +6,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
+using osuTK;
namespace osu.Game.Overlays.Settings
{
@@ -27,6 +28,11 @@ public IBindableList ItemSource
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 @@ protected class DropdownControl : OsuDropdown
{
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 9987a0c6076d..199ba14b48c7 100755
--- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
@@ -16,7 +16,6 @@ public class SettingsEnumDropdown : SettingsDropdown
{
public DropdownControl()
{
- Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X;
}
diff --git a/osu.Game/Overlays/Settings/SettingsHeader.cs b/osu.Game/Overlays/Settings/SettingsHeader.cs
index a7f1cef74c64..69b7b69a2902 100755
--- a/osu.Game/Overlays/Settings/SettingsHeader.cs
+++ b/osu.Game/Overlays/Settings/SettingsHeader.cs
@@ -22,7 +22,7 @@ public SettingsHeader(LocalisableString heading, LocalisableString subheading)
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -39,7 +39,7 @@ private void load(OsuColour colours)
new OsuSpriteText
{
Text = heading,
- Font = OsuFont.GetFont(size: 40),
+ Font = OsuFont.TorusAlternate.With(size: 40),
Margin = new MarginPadding
{
Left = SettingsPanel.CONTENT_MARGINS,
@@ -48,7 +48,7 @@ private void load(OsuColour colours)
},
new OsuSpriteText
{
- Colour = colours.Pink,
+ Colour = colourProvider.Content2,
Text = subheading,
Font = OsuFont.GetFont(size: 18),
Margin = new MarginPadding
diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
index 2fbe5224795e..d36aa2bfc20b 100755
--- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs
+++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
@@ -36,6 +36,7 @@ public NumberControl()
{
numberBox = new OutlinedNumberBox
{
+ LengthLimit = 9, // limited to less than a value that could overflow int32 backing.
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
CommitOnFocusLost = true
diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs
index 2a6f3f5ed715..0ae353602e8f 100755
--- a/osu.Game/Overlays/Settings/SettingsSection.cs
+++ b/osu.Game/Overlays/Settings/SettingsSection.cs
@@ -12,7 +12,7 @@
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
-using osuTK.Graphics;
+using osuTK;
namespace osu.Game.Overlays.Settings
{
@@ -31,9 +31,10 @@ public abstract class SettingsSection : Container, IHasFilterableChildren
public IEnumerable FilterableChildren => Children.OfType();
public virtual IEnumerable FilterTerms => new[] { Header.ToString() };
- private const int header_size = 26;
- private const int margin = 20;
- private const int border_size = 2;
+ public const int ITEM_SPACING = 14;
+
+ private const int header_size = 24;
+ private const int border_size = 4;
public bool MatchingFilter
{
@@ -54,8 +55,9 @@ protected SettingsSection()
{
Margin = new MarginPadding
{
- Top = header_size
+ Top = 36
},
+ Spacing = new Vector2(0, ITEM_SPACING),
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
@@ -63,14 +65,14 @@ protected SettingsSection()
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
AddRangeInternal(new Drawable[]
{
new Box
{
Name = "separator",
- Colour = new Color4(0, 0, 0, 255),
+ Colour = colourProvider.Background6,
RelativeSizeAxes = Axes.X,
Height = border_size,
},
@@ -78,8 +80,8 @@ private void load(OsuColour colours)
{
Padding = new MarginPadding
{
- Top = margin + border_size,
- Bottom = margin + 10,
+ Top = 28,
+ Bottom = 40,
},
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -87,13 +89,11 @@ private void load(OsuColour colours)
{
header = new OsuSpriteText
{
- Font = OsuFont.GetFont(size: header_size),
+ Font = OsuFont.TorusAlternate.With(size: header_size),
Text = Header,
- Colour = colours.Yellow,
Margin = new MarginPadding
{
- Left = SettingsPanel.CONTENT_MARGINS,
- Right = SettingsPanel.CONTENT_MARGINS
+ Horizontal = SettingsPanel.CONTENT_MARGINS
}
},
FlowContent
diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs
index 9fc3379b946b..bb9c0dd4d7e5 100755
--- a/osu.Game/Overlays/Settings/SettingsSlider.cs
+++ b/osu.Game/Overlays/Settings/SettingsSlider.cs
@@ -19,7 +19,7 @@ public class SettingsSlider : SettingsItem
{
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/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs
index 718346be3660..c2cf08ac9833 100755
--- a/osu.Game/Overlays/Settings/SettingsSubsection.cs
+++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs
@@ -46,13 +46,17 @@ protected SettingsSubsection()
FlowContent = new FillFlowContainer
{
+ Margin = new MarginPadding { Top = SettingsSection.ITEM_SPACING },
Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 8),
+ Spacing = new Vector2(0, SettingsSection.ITEM_SPACING),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
}
+ private const int header_height = 43;
+ private const int header_font_size = 20;
+
[BackgroundDependencyLoader]
private void load()
{
@@ -60,9 +64,9 @@ private void load()
{
new OsuSpriteText
{
- Text = Header, // TODO: Add localisation support after https://github.com/ppy/osu-framework/pull/4603 is merged.
- Margin = new MarginPadding { Vertical = 30, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS },
- Font = OsuFont.GetFont(weight: FontWeight.Bold),
+ Text = Header,
+ Margin = new MarginPadding { Vertical = (header_height - header_font_size) * 0.5f, Horizontal = SettingsPanel.CONTENT_MARGINS },
+ Font = OsuFont.GetFont(size: header_font_size),
},
FlowContent
});
diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs
index d28dbf106848..68562802cf85 100755
--- a/osu.Game/Overlays/Settings/SettingsTextBox.cs
+++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Overlays.Settings
@@ -13,5 +15,17 @@ public class SettingsTextBox : SettingsItem
RelativeSizeAxes = Axes.X,
CommitOnFocusLost = true
};
+
+ public override Bindable Current
+ {
+ get => base.Current;
+ set
+ {
+ if (value.Default == null)
+ throw new InvalidOperationException($"Bindable settings of type {nameof(Bindable)} should have a non-null default value.");
+
+ base.Current = value;
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs
index 4ca6e2ec423e..93b1b19b1719 100755
--- a/osu.Game/Overlays/Settings/Sidebar.cs
+++ b/osu.Game/Overlays/Settings/Sidebar.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -15,22 +16,23 @@
namespace osu.Game.Overlays.Settings
{
- public class Sidebar : Container, IStateful
+ public class Sidebar : Container, IStateful
{
- private readonly FillFlowContainer content;
- public const float DEFAULT_WIDTH = Toolbar.Toolbar.HEIGHT * 1.4f;
+ private readonly Box background;
+ private readonly FillFlowContainer content;
+ public const float DEFAULT_WIDTH = 70;
public const int EXPANDED_WIDTH = 200;
public event Action StateChanged;
- protected override Container Content => content;
+ protected override Container Content => content;
public Sidebar()
{
RelativeSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
- new Box
+ background = new Box
{
Colour = OsuColour.Gray(0.02f),
RelativeSizeAxes = Axes.Both,
@@ -39,7 +41,7 @@ public Sidebar()
{
Children = new[]
{
- content = new FillFlowContainer
+ content = new FillFlowContainer
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
@@ -52,6 +54,12 @@ public Sidebar()
};
}
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ background.Colour = colourProvider.Background5;
+ }
+
private ScheduledDelegate expandEvent;
private ExpandedState state;
@@ -80,8 +88,6 @@ private class SidebarScrollContainer : OsuScrollContainer
{
public SidebarScrollContainer()
{
- Content.Anchor = Anchor.CentreLeft;
- Content.Origin = Anchor.CentreLeft;
RelativeSizeAxes = Axes.Both;
ScrollbarVisible = false;
}
diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs
index cf6a313a1fe7..1a34143e1f34 100755
--- a/osu.Game/Overlays/Settings/SidebarButton.cs
+++ b/osu.Game/Overlays/Settings/SidebarButton.cs
@@ -1,110 +1,40 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osuTK;
-using osuTK.Graphics;
using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
+using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings
{
- public class SidebarButton : OsuButton
+ public abstract class SidebarButton : OsuButton
{
- private readonly ConstrainedIconContainer iconContainer;
- private readonly SpriteText headerText;
- private readonly Box selectionIndicator;
- private readonly Container text;
+ protected const double FADE_DURATION = 500;
- // always consider as part of flow, even when not visible (for the sake of the initial animation).
- public override bool IsPresent => true;
+ [Resolved]
+ protected OverlayColourProvider ColourProvider { get; private set; }
- private SettingsSection section;
-
- public SettingsSection Section
+ [BackgroundDependencyLoader]
+ private void load()
{
- get => section;
- set
- {
- section = value;
- headerText.Text = value.Header;
- iconContainer.Icon = value.CreateIcon();
- }
+ BackgroundColour = ColourProvider.Background5;
}
- private bool selected;
-
- public bool Selected
+ protected override void LoadComplete()
{
- get => selected;
- set
- {
- selected = value;
-
- if (selected)
- {
- selectionIndicator.FadeIn(50);
- text.FadeColour(Color4.White, 50);
- }
- else
- {
- selectionIndicator.FadeOut(50);
- text.FadeColour(OsuColour.Gray(0.6f), 50);
- }
- }
+ base.LoadComplete();
+ UpdateState();
+ FinishTransforms(true);
}
- public SidebarButton()
+ protected override bool OnHover(HoverEvent e)
{
- Height = Sidebar.DEFAULT_WIDTH;
- RelativeSizeAxes = Axes.X;
-
- BackgroundColour = Color4.Black;
-
- AddRange(new Drawable[]
- {
- text = new Container
- {
- Width = Sidebar.DEFAULT_WIDTH,
- RelativeSizeAxes = Axes.Y,
- Colour = OsuColour.Gray(0.6f),
- Children = new Drawable[]
- {
- headerText = new OsuSpriteText
- {
- Position = new Vector2(Sidebar.DEFAULT_WIDTH + 10, 0),
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- },
- iconContainer = new ConstrainedIconContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(20),
- },
- }
- },
- selectionIndicator = new Box
- {
- Alpha = 0,
- RelativeSizeAxes = Axes.Y,
- Width = 5,
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- },
- });
+ UpdateState();
+ return false;
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- selectionIndicator.Colour = colours.Yellow;
- }
+ protected override void OnHoverLost(HoverLostEvent e) => UpdateState();
+
+ protected abstract void UpdateState();
}
}
diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs
new file mode 100644
index 000000000000..fd57996b1bdf
--- /dev/null
+++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs
@@ -0,0 +1,130 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osuTK;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.Containers;
+
+namespace osu.Game.Overlays.Settings
+{
+ public class SidebarIconButton : SidebarButton
+ {
+ private const float selection_indicator_height_active = 18;
+ private const float selection_indicator_height_inactive = 4;
+
+ private readonly ConstrainedIconContainer iconContainer;
+ private readonly SpriteText headerText;
+ private readonly CircularContainer selectionIndicator;
+ private readonly Container textIconContent;
+
+ // always consider as part of flow, even when not visible (for the sake of the initial animation).
+ public override bool IsPresent => true;
+
+ private SettingsSection section;
+
+ public SettingsSection Section
+ {
+ get => section;
+ set
+ {
+ section = value;
+ headerText.Text = value.Header;
+ iconContainer.Icon = value.CreateIcon();
+ }
+ }
+
+ private bool selected;
+
+ public bool Selected
+ {
+ get => selected;
+ set
+ {
+ selected = value;
+
+ if (IsLoaded)
+ UpdateState();
+ }
+ }
+
+ public SidebarIconButton()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 46;
+
+ AddRange(new Drawable[]
+ {
+ textIconContent = new Container
+ {
+ Width = Sidebar.DEFAULT_WIDTH,
+ RelativeSizeAxes = Axes.Y,
+ Colour = OsuColour.Gray(0.6f),
+ Children = new Drawable[]
+ {
+ headerText = new OsuSpriteText
+ {
+ Position = new Vector2(Sidebar.DEFAULT_WIDTH + 10, 0),
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ iconContainer = new ConstrainedIconContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(20),
+ },
+ }
+ },
+ selectionIndicator = new CircularContainer
+ {
+ Alpha = 0,
+ Width = 4,
+ Height = selection_indicator_height_inactive,
+ Masking = true,
+ CornerRadius = 1.5f,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Margin = new MarginPadding
+ {
+ Left = 9,
+ },
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.White
+ }
+ },
+ });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ selectionIndicator.Colour = ColourProvider.Highlight1;
+ }
+
+ protected override void UpdateState()
+ {
+ if (Selected)
+ {
+ textIconContent.FadeColour(ColourProvider.Content1, FADE_DURATION, Easing.OutQuint);
+
+ selectionIndicator.FadeIn(FADE_DURATION, Easing.OutQuint);
+ selectionIndicator.ResizeHeightTo(selection_indicator_height_active, FADE_DURATION, Easing.OutElasticHalf);
+ }
+ else
+ {
+ textIconContent.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint);
+
+ selectionIndicator.FadeOut(FADE_DURATION, Easing.OutQuint);
+ selectionIndicator.ResizeHeightTo(selection_indicator_height_inactive, FADE_DURATION, Easing.OutQuint);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs
index 220d7baa3cf1..42560c18d6ed 100755
--- a/osu.Game/Overlays/SettingsOverlay.cs
+++ b/osu.Game/Overlays/SettingsOverlay.cs
@@ -25,12 +25,13 @@ public class SettingsOverlay : SettingsPanel, INamedOverlayComponent
{
new MfSection(createSubPanel(new MfSettingsPanel())),
new GeneralSection(),
- new GraphicsSection(),
- new AudioSection(),
+ new SkinSection(),
new InputSection(createSubPanel(new KeyBindingPanel())),
new UserInterfaceSection(),
new GameplaySection(),
- new SkinSection(),
+ new RulesetSection(),
+ new AudioSection(),
+ new GraphicsSection(),
new OnlineSection(),
new MaintenanceSection(),
new DebugSection(),
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index bda4bb5ece1b..0ceb7fc50dab 100755
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -6,7 +6,6 @@
using System.Linq;
using System.Threading.Tasks;
using osuTK;
-using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -15,7 +14,6 @@
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
@@ -25,7 +23,7 @@ namespace osu.Game.Overlays
[Cached]
public abstract class SettingsPanel : OsuFocusedOverlayContainer
{
- public const float CONTENT_MARGINS = 15;
+ public const float CONTENT_MARGINS = 20;
public const float TRANSITION_LENGTH = 600;
@@ -46,7 +44,7 @@ public abstract class SettingsPanel : OsuFocusedOverlayContainer
protected override Container Content => ContentContainer;
protected Sidebar Sidebar;
- private SidebarButton selectedSidebarButton;
+ private SidebarIconButton selectedSidebarButton;
public SettingsSectionsContainer SectionsContainer { get; private set; }
@@ -64,6 +62,9 @@ public abstract class SettingsPanel : OsuFocusedOverlayContainer
public IBindable CurrentSection = new Bindable();
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
protected SettingsPanel(bool showSidebar)
{
this.showSidebar = showSidebar;
@@ -89,7 +90,7 @@ private void load()
Origin = Anchor.TopRight,
Scale = new Vector2(2, 1), // over-extend to the left for transitions
RelativeSizeAxes = Axes.Both,
- Colour = OsuColour.Gray(0.05f),
+ Colour = colourProvider.Background4,
Alpha = 1,
},
loading = new LoadingLayer
@@ -105,17 +106,23 @@ private void load()
RelativeSizeAxes = Axes.Both,
ExpandableHeader = CreateHeader(),
SelectedSection = { BindTarget = CurrentSection },
- FixedHeader = searchTextBox = new SeekLimitedSearchTextBox
+ FixedHeader = new Container
{
RelativeSizeAxes = Axes.X,
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- Width = 0.95f,
- Margin = new MarginPadding
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding
{
- Top = 20,
- Bottom = 20
+ Vertical = 20,
+ Horizontal = CONTENT_MARGINS
},
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Child = searchTextBox = new SeekLimitedSearchTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ }
},
Footer = CreateFooter().With(f => f.Alpha = 0)
});
@@ -245,11 +252,11 @@ private void loadSidebarButtons()
});
}
- private IEnumerable createSidebarButtons()
+ private IEnumerable createSidebarButtons()
{
foreach (var section in SectionsContainer)
{
- yield return new SidebarButton
+ yield return new SidebarIconButton
{
Section = section,
Action = () =>
@@ -292,11 +299,12 @@ protected override FlowContainer CreateScrollContentContainer()
Direction = FillDirection.Vertical,
};
- public SettingsSectionsContainer()
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
{
HeaderBackground = new Box
{
- Colour = Color4.Black,
+ Colour = colourProvider.Background4,
RelativeSizeAxes = Axes.Both
};
}
diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs
index 6c268de60ae5..9438999df624 100755
--- a/osu.Game/Overlays/SettingsSubPanel.cs
+++ b/osu.Game/Overlays/SettingsSubPanel.cs
@@ -7,10 +7,8 @@
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Overlays
{
@@ -34,18 +32,18 @@ private void load()
protected override bool DimMainContent => false; // dimming is handled by main overlay
- private class BackButton : OsuButton
+ private class BackButton : SidebarButton
{
+ private Container content;
+
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(Sidebar.DEFAULT_WIDTH);
- BackgroundColour = Color4.Black;
-
AddRange(new Drawable[]
{
- new Container
+ content = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -71,6 +69,11 @@ private void load()
}
});
}
+
+ protected override void UpdateState()
+ {
+ content.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint);
+ }
}
}
}
diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
index a0ec8e3e0ef9..eec71a36234f 100755
--- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
+++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
@@ -47,9 +47,34 @@ protected override void PerformLoad()
}
}
+ 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 @@ protected override void AddBindable(TLookup lookup, Bindable
{
- realmFactory?.Context.Write(() => setting.Value = b.NewValue.ToString());
+ lock (pendingWrites)
+ pendingWrites.Add(lookup);
};
}
}
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index 1143549f7f5b..200bbf3f929e 100755
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -258,6 +258,13 @@ public ControlPointInfo ControlPointInfo
}
public BeatmapMetadata Metadata => baseBeatmap.Metadata;
+
+ public BeatmapDifficulty Difficulty
+ {
+ get => baseBeatmap.Difficulty;
+ set => baseBeatmap.Difficulty = value;
+ }
+
public List Breaks => baseBeatmap.Breaks;
public double TotalBreakTime => baseBeatmap.TotalBreakTime;
public IEnumerable GetStatistics() => baseBeatmap.GetStatistics();
diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs
index 73bab31e8278..d8babf2f3224 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs
@@ -27,7 +27,7 @@ public abstract class StrainDecaySkill : StrainSkill
///
/// The current strain level.
///
- protected double CurrentStrain { get; private set; } = 1;
+ protected double CurrentStrain { get; private set; }
protected StrainDecaySkill(Mod[] mods)
: base(mods)
diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
index 0880f1b08e97..bbd2f079aaae 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
@@ -25,7 +25,7 @@ public abstract class StrainSkill : Skill
///
protected virtual int SectionLength => 400;
- private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
+ private double currentSectionPeak; // We also keep track of the peak strain level in the current section.
private double currentSectionEnd;
diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
index 81f480878957..6ed91e983ae0 100644
--- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
+++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
@@ -24,6 +24,11 @@ public class BeatmapVerifier : IBeatmapVerifier
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 000000000000..ac2542beb020
--- /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 000000000000..57f7c609167a
--- /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 000000000000..3a994fabfa0a
--- /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 fc2541d56cb0..ea925192c144 100755
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -13,7 +13,6 @@
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 @@ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpac
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.BeatmapInfo.BaseDifficulty.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 @@ protected HitObjectComposer()
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 4664f3808c19..743a2f41fcd2 100755
--- 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 @@ public interface IPositionSnapProvider
///
/// 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/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
index b7529f39ca14..0c0c5990d1f2 100755
--- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
@@ -112,7 +112,7 @@ public virtual void UpdateTimeAndPosition(SnapResult result)
/// Invokes ,
/// refreshing and parameters for the .
///
- protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty);
+ protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) ?? false;
diff --git a/osu.Game/Rulesets/Mods/DifficultyBindable.cs b/osu.Game/Rulesets/Mods/DifficultyBindable.cs
index e4304795f264..6cfae0b08560 100644
--- a/osu.Game/Rulesets/Mods/DifficultyBindable.cs
+++ b/osu.Game/Rulesets/Mods/DifficultyBindable.cs
@@ -27,7 +27,7 @@ public class DifficultyBindable : Bindable
///
/// A function that can extract the current value of this setting from a beatmap difficulty for display purposes.
///
- public Func ReadCurrentFromDifficulty;
+ public Func ReadCurrentFromDifficulty;
public float Precision
{
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index 0b159819d48e..035ebe10cb9f 100755
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -67,7 +67,8 @@ public IList Samples
}
}
- public SampleControlPoint SampleControlPoint;
+ public SampleControlPoint SampleControlPoint = SampleControlPoint.DEFAULT;
+ public DifficultyControlPoint DifficultyControlPoint = DifficultyControlPoint.DEFAULT;
///
/// Whether this is in Kiai time.
@@ -94,6 +95,12 @@ public HitObject()
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 @@ public HitObject()
/// The cancellation token.
public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default)
{
- ApplyDefaultsToSelf(controlPointInfo, difficulty);
+ var legacyInfo = controlPointInfo as LegacyControlPointInfo;
- if (controlPointInfo is LegacyControlPointInfo legacyInfo)
+ 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);
+ DifficultyControlPoint = (DifficultyControlPoint)legacyInfo.DifficultyPointAt(StartTime).DeepClone();
+ DifficultyControlPoint.Time = StartTime;
}
- else
+
+ ApplyDefaultsToSelf(controlPointInfo, difficulty);
+
+ // This is done here after ApplyDefaultsToSelf as we may require custom defaults to be applied to have an accurate end time.
+ if (legacyInfo != null)
{
- 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 e1de82ade714..ad191f7ff51d 100755
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
@@ -43,9 +43,8 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I
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/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs
index 85693abb9397..dfeb6b47883c 100755
--- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs
@@ -100,7 +100,7 @@ public override void ApplyBeatmap(IBeatmap beatmap)
.First()
)));
- targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target);
+ targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, min_health_target, mid_health_target, max_health_target);
// Add back a portion of the amount of HP to be drained, depending on the lenience requested.
targetMinimumHealth += drainLenience * (1 - targetMinimumHealth);
diff --git a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
index dcd2cc8b5537..23325bcd13a7 100755
--- a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
+++ b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
@@ -7,7 +7,7 @@
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 @@ public class MultiplierControlPoint : IComparable
///
/// 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 @@ public class MultiplierControlPoint : IComparable
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 e9865f6c8b79..c0b339a2316a 100755
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -55,7 +55,10 @@ public FrameStabilityContainer(double gameplayStartTime = double.MinValue)
///
/// 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 @@ private void updateClock()
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 7b30bb9574ff..2a9d3d1cf01c 100755
--- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
@@ -133,32 +133,39 @@ private void load()
maxDuration = duration;
// The slider multiplier is post-multiplied to determine the final velocity, but for relative scale beat lengths
// the multiplier should not affect the effective timing point (the longest in the beatmap), so it is factored out here
- baseBeatLength = timingPoints[i].BeatLength / Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
+ baseBeatLength = timingPoints[i].BeatLength / Beatmap.Difficulty.SliderMultiplier;
}
}
}
// 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.BeatmapInfo.BaseDifficulty.SliderMultiplier,
+ Velocity = Beatmap.Difficulty.SliderMultiplier,
BaseBeatLength = baseBeatLength,
TimingPoint = lastTimingPoint,
- DifficultyPoint = lastDifficultyPoint
+ EffectPoint = lastEffectPoint
};
});
@@ -172,7 +179,7 @@ private void load()
ControlPoints.AddRange(timingChanges);
if (ControlPoints.Count == 0)
- ControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier });
+ ControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.Difficulty.SliderMultiplier });
}
protected override void LoadComplete()
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index c8c53388afae..bf4f3ef4f417 100755
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -25,7 +25,7 @@
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;
@@ -72,9 +72,12 @@ public async Task OrderByTotalScoreAsync(ScoreInfo[] scores, Cancel
}
}
- // We're calling .Result, but this should not be a blocking call due to the above GetDifficultyAsync() calls.
- return scores.OrderByDescending(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken).Result)
- .ThenBy(s => s.OnlineScoreID)
+ var totalScores = await Task.WhenAll(scores.Select(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken))).ConfigureAwait(false);
+
+ return scores.Select((score, index) => (score, totalScore: totalScores[index]))
+ .OrderByDescending(g => g.totalScore)
+ .ThenBy(g => g.score.OnlineScoreID)
+ .Select(g => g.score)
.ToArray();
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
index 730f482f83fd..6b32ff96c4ef 100755
--- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
@@ -5,14 +5,15 @@
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 @@ public override (Vector2 position, double time) GetSnappedPosition(Vector2 posit
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 59f88ac6417a..9d43e3258afa 100755
--- 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.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 @@ public abstract class DistanceSnapGrid : CompositeDrawable
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 @@ protected override void LoadComplete()
private void updateSpacing()
{
- DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime);
+ DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject);
if (endTime == null)
MaxIntervals = int.MaxValue;
@@ -88,7 +94,7 @@ private void updateSpacing()
{
// +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 324893676507..21457ea273ca 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 000000000000..6b62459c9757
--- /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 9461f5e885f1..6a26f69e412c 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);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ this.ShowPopover();
+ return true;
+ }
+
+ private void updateText()
+ {
+ Label.Text = $"{bank.Value} {volume.Value}";
+ }
+
+ public Popover GetPopover() => new SampleEditPopover(hitObject);
- Origin = Anchor.BottomCentre;
- Anchor = Anchor.BottomCentre;
+ public class SampleEditPopover : OsuPopover
+ {
+ private readonly HitObject hitObject;
+ private readonly SampleControlPoint point;
- AutoSizeAxes = Axes.X;
- RelativeSizeAxes = Axes.Y;
+ private LabelledTextBox bank;
+ private SliderWithTextBoxInput volume;
- Color4 colour = samplePoint.GetRepresentingColour(colours);
+ [Resolved(canBeNull: true)]
+ private EditorBeatmap beatmap { get; set; }
- InternalChildren = new Drawable[]
+ public SampleEditPopover(HitObject hitObject)
{
- volumeBox = new Circle
- {
- CornerRadius = 5,
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Y = -20,
- Width = 10,
- Colour = colour,
- },
- new Container
+ this.hitObject = hitObject;
+ point = hitObject.SampleControlPoint;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
{
- 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,
+ }
}
}
- },
- };
+ };
+
+ bank.Current = point.SampleBankBindable;
+ bank.Current.BindValueChanged(_ => beatmap.Update(hitObject));
- volume.BindValueChanged(volume => volumeBox.Height = max_volume_height * volume.NewValue / 100f, true);
- bank.BindValueChanged(bank => text.Text = bank.NewValue, true);
+ 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 712afc4de77c..3d4911b20e6f 100755
--- 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.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 @@ public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
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 @@ protected override void LoadComplete()
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 @@ public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition
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();
}
}
\ No newline at end of file
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
index c4beb40f921e..2b2e66fb1849 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 @@ protected override void LoadComplete()
{
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 911c9fea5133..e2458d45c9fb 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.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 @@ private void updateColour()
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 @@ protected override void Update()
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 @@ protected override bool OnDragStart(DragStartEvent e)
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 b213f5b5f6c0..e073144b863f 100755
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -162,7 +162,7 @@ private void load(OsuColour colours, OsuConfigManager config)
// 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 @@ protected void Save()
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();
}
@@ -508,6 +508,7 @@ public override bool OnExiting(IScreen next)
if (isNewBeatmap || HasUnsavedChanges)
{
+ samplePlaybackDisabled.Value = true;
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave, cancelExit));
return true;
}
@@ -522,7 +523,10 @@ public override bool OnExiting(IScreen next)
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);
}
@@ -756,7 +760,11 @@ private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo)
ClipboardContent = editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? clipboard.Value : string.Empty
});
- private void cancelExit() => loader?.CancelPendingDifficultySwitch();
+ private void cancelExit()
+ {
+ samplePlaybackDisabled.Value = false;
+ loader?.CancelPendingDifficultySwitch();
+ }
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index 3402bf653aaf..2e84ef437a44 100755
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -10,6 +10,7 @@
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 @@ public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapPr
///
public readonly Bindable PlacementObject = new Bindable();
+ private readonly BeatmapInfo beatmapInfo;
public readonly IBeatmap PlayableBeatmap;
///
@@ -66,9 +68,37 @@ public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapPr
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,17 @@ public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null)
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
+ {
+ get => PlayableBeatmap.Difficulty;
+ set => PlayableBeatmap.Difficulty = value;
+ }
public ControlPointInfo ControlPointInfo
{
@@ -286,7 +322,7 @@ protected override void UpdateState()
///
public void Clear() => RemoveRange(HitObjects.ToArray());
- private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty);
+ private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, PlayableBeatmap.Difficulty);
private void trackStartTime(HitObject hitObject)
{
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index 25806abcc7ae..2352fc1390ce 100755
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -25,7 +25,9 @@ public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourc
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 @@ public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourc
///
public bool IsSeeking { get; private set; }
- public EditorClock(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
- : this(beatmap.ControlPointInfo, beatDivisor)
- {
- }
-
- public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor)
+ public EditorClock(IBeatmap beatmap = null, BindableBeatDivisor beatDivisor = null)
{
- this.beatDivisor = beatDivisor;
+ Beatmap = beatmap ?? new Beatmap();
- 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 b271a145f528..508663224d74 100644
--- a/osu.Game/Screens/Edit/EditorRoundedScreen.cs
+++ b/osu.Game/Screens/Edit/EditorRoundedScreen.cs
@@ -41,7 +41,7 @@ private void load()
{
new Box
{
- Colour = ColourProvider.Dark4,
+ Colour = ColourProvider.Background3,
RelativeSizeAxes = Axes.Both,
},
roundedContent = new Container
diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs
index b9483c175004..9025375198f5 100644
--- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs
+++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs
@@ -30,7 +30,7 @@ private void load()
Label = "物件大小(CS)",
FixedLabelWidth = LABEL_WIDTH,
Description = "这将决定所有物件的大小, 数值越大, 物件越小",
- Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.CircleSize)
+ Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
@@ -43,7 +43,7 @@ private void load()
Label = "掉血速度(HP)",
FixedLabelWidth = LABEL_WIDTH,
Description = "这将决定掉血的速度, 数值越大, 掉血越快",
- Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.DrainRate)
+ Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
@@ -56,7 +56,7 @@ private void load()
Label = "缩圈速度(AR)",
FixedLabelWidth = LABEL_WIDTH,
Description = "这将决定缩圈速度, 数值越大, 缩圈越快",
- Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate)
+ Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
@@ -69,7 +69,7 @@ private void load()
Label = "整体难度(OD)",
FixedLabelWidth = LABEL_WIDTH,
Description = "这将决定该谱面的整体难度(准确率要求, 转盘难度等), 数值越大, 难度越高",
- Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty)
+ Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
@@ -87,10 +87,10 @@ private void onValueChanged(ValueChangedEvent args)
{
// for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
- Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value;
- Beatmap.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value;
- Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value;
- Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
+ Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
+ Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
+ Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
+ Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.UpdateAllHitObjects();
}
diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
index 9e88fa92a338..b5319439c940 100644
--- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs
+++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
@@ -10,7 +10,7 @@
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 48639789af2a..938c7f9cf0b3 100755
--- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
+++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
@@ -12,8 +12,6 @@ public class ControlPointSettings : EditorRoundedScreenSettings
{
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 100755
index 33b169ad5823..000000000000
--- 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("速度倍率")
- {
- 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 2c83134fe66c..b7837e905f05 100755
--- 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 @@ internal class EffectSection : Section
private LabelledSwitchButton kiai;
private LabelledSwitchButton omitBarLine;
+ private SliderWithTextBoxInput scrollSpeedSlider;
+
[BackgroundDependencyLoader]
private void load()
{
- Flow.AddRange(new[]
+ Flow.AddRange(new Drawable[]
{
- kiai = new LabelledSwitchButton { Label = "高潮/副歌" },
- omitBarLine = new LabelledSwitchButton { Label = "移除第一条横线" },
+ kiai = new LabelledSwitchButton { Label = "高潮、副歌" },
+ omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" },
+ scrollSpeedSlider = new SliderWithTextBoxInput("滚动速度")
+ {
+ Current = new EffectControlPoint().ScrollSpeedBindable,
+ KeyboardStep = 0.1f
+ }
});
}
@@ -32,6 +40,9 @@ protected override void OnControlPointChanged(ValueChangedEvent ChangeHandler?.SaveState());
+
+ scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
+ scrollSpeedSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
}
}
@@ -42,7 +53,8 @@ protected override EffectControlPoint CreatePoint()
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 7b553ac7ad09..a8de476d674a 100644
--- a/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs
+++ b/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs
@@ -18,7 +18,7 @@ public class DifficultyRowAttribute : RowAttribute
public DifficultyRowAttribute(DifficultyControlPoint difficulty)
: base(difficulty, "difficulty")
{
- speedMultiplier = difficulty.SpeedMultiplierBindable.GetBoundCopy();
+ speedMultiplier = difficulty.SliderVelocityBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
@@ -32,7 +32,7 @@ private void load()
},
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 812407d6daed..1b33fd62aa18 100644
--- a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs
+++ b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs
@@ -12,14 +12,18 @@ public class EffectRowAttribute : RowAttribute
{
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 @@ private void load()
{
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 100755
index bcc07107a100..000000000000
--- 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("音量")
- {
- 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/Loader.cs b/osu.Game/Screens/Loader.cs
index 90d80ddb1ba5..6ba9cdfadf67 100755
--- a/osu.Game/Screens/Loader.cs
+++ b/osu.Game/Screens/Loader.cs
@@ -47,23 +47,25 @@ private IntroScreen getIntroSequence()
switch (introSequence)
{
case IntroSequence.Circles:
- return new IntroCircles(false);
+ return new IntroCircles(createMainMenu);
case IntroSequence.CirclesCN:
- return new IntroCircles(true);
+ return new IntroCircles(createMainMenu, true);
case IntroSequence.TrianglesCN:
- return new IntroTriangles(true);
+ return new IntroTriangles(createMainMenu, true);
case IntroSequence.SkippedIntro:
- return new IntroSkipped();
+ return new IntroSkipped(createMainMenu);
case IntroSequence.Welcome:
- return new IntroWelcome();
+ return new IntroWelcome(createMainMenu);
default:
- return new IntroTriangles();
+ return new IntroTriangles(createMainMenu);
}
+
+ MainMenu createMainMenu() => new MainMenu();
}
protected virtual ShaderPrecompiler CreateShaderPrecompiler() => new ShaderPrecompiler();
diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs
index 15bd0c5a8fba..78f9929c77cd 100644
--- a/osu.Game/Screens/Menu/IntroCircles.cs
+++ b/osu.Game/Screens/Menu/IntroCircles.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -24,7 +26,8 @@ public class IntroCircles : IntroScreen
private Sample welcome;
private readonly bool useTranslate;
- public IntroCircles(bool useTranslate = false)
+ public IntroCircles([CanBeNull] Func createNextScreen = null, bool useTranslate = false)
+ : base(createNextScreen)
{
this.useTranslate = useTranslate;
}
diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs
index e1835fe004a1..f5dad2e54f6c 100755
--- a/osu.Game/Screens/Menu/IntroScreen.cs
+++ b/osu.Game/Screens/Menu/IntroScreen.cs
@@ -1,16 +1,18 @@
// 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;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Utils;
using osu.Framework.Screens;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.IO.Archives;
@@ -56,7 +58,7 @@ public abstract class IntroScreen : StartupScreen
private LeasedBindable beatmap;
- private MainMenu mainMenu;
+ private OsuScreen nextScreen;
protected BeatmapSetInfo SetInfo;
@@ -74,12 +76,20 @@ public abstract class IntroScreen : StartupScreen
private BeatmapManager beatmaps { get; set; }
+ [CanBeNull]
+ private readonly Func createNextScreen;
+
///
/// Whether the is provided by osu! resources, rather than a user beatmap.
/// Only valid during or after .
///
protected bool UsingThemedIntro { get; private set; }
+ protected IntroScreen([CanBeNull] Func createNextScreen = null)
+ {
+ this.createNextScreen = createNextScreen;
+ }
+
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, SkinManager skinManager, BeatmapManager beatmaps, Framework.Game game)
{
@@ -242,14 +252,21 @@ protected override void LogoArriving(OsuLogo logo, bool resuming)
}
}
- protected void PrepareMenuLoad() => LoadComponentAsync(mainMenu = new MainMenu());
+ protected void PrepareMenuLoad()
+ {
+ nextScreen = createNextScreen?.Invoke();
+
+ if (nextScreen != null)
+ LoadComponentAsync(nextScreen);
+ }
protected virtual void LoadMenu()
{
beatmap.Return();
DidLoadMenu = true;
- this.Push(mainMenu);
+ if (nextScreen != null)
+ this.Push(nextScreen);
}
}
}
diff --git a/osu.Game/Screens/Menu/IntroSkipped.cs b/osu.Game/Screens/Menu/IntroSkipped.cs
index bbdb287dfed0..01963219b407 100644
--- a/osu.Game/Screens/Menu/IntroSkipped.cs
+++ b/osu.Game/Screens/Menu/IntroSkipped.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;
using osu.Framework.Screens;
using osu.Framework.Graphics;
using osu.Framework.Allocation;
@@ -26,6 +27,11 @@ public class IntroSkipped : IntroScreen
[Resolved(CanBeNull = true)]
private OsuGame game { get; set; }
+ public IntroSkipped(Func createScreen)
+ : base(createScreen)
+ {
+ }
+
[BackgroundDependencyLoader]
private void load(MConfigManager config)
{
diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs
index d824536db36d..bc8cf160ce5d 100644
--- a/osu.Game/Screens/Menu/IntroTriangles.cs
+++ b/osu.Game/Screens/Menu/IntroTriangles.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -42,14 +43,15 @@ public class IntroTriangles : IntroScreen
private Sample welcome;
private readonly bool useTranslate;
- public IntroTriangles(bool useTranslate = false)
+ private DecoupleableInterpolatingFramedClock decoupledClock;
+ private TrianglesIntroSequence intro;
+
+ public IntroTriangles([CanBeNull] Func createNextScreen = null, bool useTranslate = false)
+ : base(createNextScreen)
{
this.useTranslate = useTranslate;
}
- private DecoupleableInterpolatingFramedClock decoupledClock;
- private TrianglesIntroSequence intro;
-
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs
index f74043b0456d..639591cfef4d 100644
--- a/osu.Game/Screens/Menu/IntroWelcome.cs
+++ b/osu.Game/Screens/Menu/IntroWelcome.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using JetBrains.Annotations;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -32,6 +34,11 @@ public class IntroWelcome : IntroScreen
private BackgroundScreenDefault background;
+ public IntroWelcome([CanBeNull] Func createNextScreen = null)
+ : base(createNextScreen)
+ {
+ }
+
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
diff --git a/osu.Game/Screens/Mvis/MvisScreen.cs b/osu.Game/Screens/Mvis/MvisScreen.cs
index fd22d8b8f386..9662179b6a64 100644
--- a/osu.Game/Screens/Mvis/MvisScreen.cs
+++ b/osu.Game/Screens/Mvis/MvisScreen.cs
@@ -914,9 +914,10 @@ public override void OnResuming(IScreen last)
public bool OnPressed(KeyBindingPressEvent action)
{
//查找本体按键绑定
- keyBindings.FirstOrDefault(b => b.Key == action.Action).Value?.Invoke();
+ var target = keyBindings.FirstOrDefault(b => b.Key == action.Action).Value;
+ target?.Invoke();
- return false;
+ return target != null;
}
public void OnReleased(KeyBindingReleaseEvent action) { }
@@ -932,9 +933,10 @@ protected override bool Handle(UIEvent e)
protected override bool OnKeyDown(KeyDownEvent e)
{
//查找插件按键绑定并执行
- pluginKeyBindings.FirstOrDefault(b => b.Key.Key == e.Key).Key?.Action?.Invoke();
+ var target = pluginKeyBindings.FirstOrDefault(b => b.Key.Key == e.Key).Key;
+ target?.Action?.Invoke();
- return base.OnKeyDown(e);
+ return target != null;
}
//当有弹窗或游戏失去焦点时要进行的动作
diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
index 381849189d11..abda9e897b03 100644
--- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
@@ -116,8 +116,6 @@ public void AddOrUpdateRoom(Room room)
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 @@ public void ClearRooms()
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 907b7e308acb..85efdcef1a89 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
@@ -129,7 +129,7 @@ private void removeRooms(IEnumerable rooms)
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 80a5daa7c8b3..0edf5dde6de0 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -7,7 +7,6 @@
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 @@ public MatchSettings(Room room)
}
[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 @@ private void load(OsuColour colours)
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 6c3dfe738227..cf1066df10e0 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
@@ -79,11 +79,13 @@ private class MultiplayerListingPollingComponent : ListingPollingComponent
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 0f256160eb0a..a380ddef25d7 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
@@ -19,15 +19,19 @@ protected override void LoadComplete()
{
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 @@ protected override void Dispose(bool isDisposing)
{
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/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
index fcbbdd56bed5..82e678815aea 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
@@ -5,7 +5,6 @@
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 @@ public MatchSettings(Room room)
}
[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 @@ private void load(OsuColour colours)
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/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs
index 71bea2a1455e..2a1c4599d5d7 100755
--- a/osu.Game/Screens/Play/FailAnimation.cs
+++ b/osu.Game/Screens/Play/FailAnimation.cs
@@ -6,11 +6,15 @@
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.Rulesets.Objects.Drawables;
using osuTK;
@@ -20,25 +24,40 @@ 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 : Component
+ public class FailAnimation : Container
{
public Action OnComplete;
private readonly DrawableRuleset drawableRuleset;
-
private readonly BindableDouble trackFreq = new BindableDouble(1);
+ private Container filters;
+
+ private Box failFlash;
+
private Track track;
+ private AudioFilter failLowPassFilter;
+ private AudioFilter failHighPassFilter;
+
private const float duration = 2500;
private Sample failSample;
+ 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]
@@ -46,6 +65,27 @@ private void load(AudioManager audio, IBindable beatmap)
{
track = beatmap.Value.Track;
failSample = audio.Samples.Get(@"Gameplay/failsound");
+
+ AddRangeInternal(new Drawable[]
+ {
+ filters = new Container
+ {
+ Children = new Drawable[]
+ {
+ failLowPassFilter = new AudioFilter(audio.TrackMixer),
+ failHighPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
+ },
+ },
+ Content,
+ failFlash = new Box
+ {
+ Colour = Color4.Red,
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ Depth = float.MinValue,
+ Alpha = 0
+ },
+ });
}
private bool started;
@@ -60,19 +100,42 @@ public void Start()
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);
+
+ failFlash.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()
@@ -121,11 +184,5 @@ private void dropOffScreen(DrawableHitObject obj, double failTime, float randomR
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/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs
index 63de5c8de599..87b19e8433ef 100644
--- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs
@@ -11,7 +11,6 @@ namespace osu.Game.Screens.Play.HUD
public class DefaultScoreCounter : GameplayScoreCounter, ISkinnableDrawable
{
public DefaultScoreCounter()
- : base(6)
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
diff --git a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs
index e09630d2c4fd..e05eff5f3ea5 100644
--- a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs
@@ -14,8 +14,8 @@ public abstract class GameplayScoreCounter : ScoreCounter
{
private Bindable scoreDisplayMode;
- protected GameplayScoreCounter(int leading = 0, bool useCommaSeparator = false)
- : base(leading, useCommaSeparator)
+ protected GameplayScoreCounter()
+ : base(6)
{
}
diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
index d04e60a2ab8c..b1c07512ddaf 100644
--- a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
@@ -4,11 +4,9 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -148,7 +146,7 @@ protected override void UpdateAfterChildren()
Score2Text.X = Math.Max(5 + Score2Text.DrawWidth / 2, score2Bar.DrawWidth);
}
- protected class MatchScoreCounter : ScoreCounter
+ protected class MatchScoreCounter : CommaSeparatedScoreCounter
{
private OsuSpriteText displayedSpriteText;
@@ -173,8 +171,6 @@ private void updateFont(bool winning)
=> displayedSpriteText.Font = winning
? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true);
-
- protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N0");
}
}
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 10be0246f05e..bf6602c7a04d 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -2,7 +2,6 @@
// 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.Threading.Tasks;
@@ -125,15 +124,11 @@ public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisab
public DimmableStoryboard DimmableStoryboard { get; private set; }
- [Cached]
- [Cached(Type = typeof(IBindable>))]
- protected new readonly Bindable> Mods = new Bindable>(Array.Empty());
-
///
/// Whether failing should be allowed.
/// By default, this checks whether all selected mods allow failing.
///
- protected virtual bool CheckModsAllowFailure() => Mods.Value.OfType().All(m => m.PerformFail());
+ protected virtual bool CheckModsAllowFailure() => GameplayState.Mods.OfType().All(m => m.PerformFail());
public readonly PlayerConfiguration Configuration;
@@ -179,12 +174,12 @@ protected virtual void PrepareReplay()
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
{
- Mods.Value = base.Mods.Value.Select(m => m.DeepClone()).ToArray();
+ var gameplayMods = Mods.Value.Select(m => m.DeepClone()).ToArray();
if (Beatmap.Value is DummyWorkingBeatmap)
return;
- IBeatmap playableBeatmap = loadPlayableBeatmap();
+ IBeatmap playableBeatmap = loadPlayableBeatmap(gameplayMods);
if (playableBeatmap == null)
return;
@@ -199,12 +194,12 @@ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
if (game is OsuGame osuGame)
LocalUserPlaying.BindTo(osuGame.LocalUserPlaying);
- DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value);
+ DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, gameplayMods);
dependencies.CacheAs(DrawableRuleset);
ScoreProcessor = ruleset.CreateScoreProcessor();
ScoreProcessor.ApplyBeatmap(playableBeatmap);
- ScoreProcessor.Mods.BindTo(Mods);
+ ScoreProcessor.Mods.Value = gameplayMods;
dependencies.CacheAs(ScoreProcessor);
@@ -218,16 +213,16 @@ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
+ AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
+
Score = CreateScore(playableBeatmap);
// ensure the score is in a consistent state with the current player.
Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo;
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
- Score.ScoreInfo.Mods = Mods.Value.ToArray();
+ Score.ScoreInfo.Mods = gameplayMods;
- dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value, Score));
-
- AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
+ dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score));
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
@@ -235,17 +230,53 @@ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
// 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)
{
@@ -302,13 +333,13 @@ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
// this is required for mods that apply transforms to these processors.
ScoreProcessor.OnLoadComplete += _ =>
{
- foreach (var mod in Mods.Value.OfType())
+ foreach (var mod in gameplayMods.OfType())
mod.ApplyToScoreProcessor(ScoreProcessor);
};
HealthProcessor.OnLoadComplete += _ =>
{
- foreach (var mod in Mods.Value.OfType())
+ foreach (var mod in gameplayMods.OfType())
mod.ApplyToHealthProcessor(HealthProcessor);
};
@@ -357,7 +388,7 @@ private Drawable createOverlayComponents(WorkingBeatmap working)
// display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
- HUDOverlay = new HUDOverlay(DrawableRuleset, Mods.Value)
+ HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods)
{
HoldToQuit =
{
@@ -381,11 +412,6 @@ private Drawable createOverlayComponents(WorkingBeatmap working)
RequestSkip = () => progressToResults(false),
Alpha = 0
},
- FailOverlay = new FailOverlay
- {
- OnRetry = Restart,
- OnQuit = () => PerformExit(true),
- },
PauseOverlay = new PauseOverlay
{
OnResume = Resume,
@@ -393,18 +419,7 @@ private Drawable createOverlayComponents(WorkingBeatmap working)
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)
@@ -416,20 +431,6 @@ private Drawable createOverlayComponents(WorkingBeatmap working)
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;
}
@@ -468,7 +469,7 @@ private void updatePauseOnFocusLostState()
}
}
- private IBeatmap loadPlayableBeatmap()
+ private IBeatmap loadPlayableBeatmap(Mod[] gameplayMods)
{
IBeatmap playable;
@@ -482,7 +483,7 @@ private IBeatmap loadPlayableBeatmap()
try
{
- playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value);
+ playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, gameplayMods);
}
catch (BeatmapInvalidForRulesetException)
{
@@ -490,7 +491,7 @@ private IBeatmap loadPlayableBeatmap()
rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset;
ruleset = rulesetInfo.CreateInstance();
- playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value);
+ playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, gameplayMods);
}
if (playable.HitObjects.Count == 0)
@@ -547,7 +548,7 @@ protected void PerformExit(bool showDialogFirst)
// 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;
}
@@ -773,7 +774,7 @@ private void progressToResults(bool withDelay)
protected FailOverlay FailOverlay { get; private set; }
- private FailAnimation failAnimation;
+ private FailAnimation failAnimationLayer;
private bool onFail()
{
@@ -789,9 +790,9 @@ private bool onFail()
if (PauseOverlay.State.Value == Visibility.Visible)
PauseOverlay.Hide();
- failAnimation.Start();
+ failAnimationLayer.Start();
- if (Mods.Value.OfType().Any(m => m.RestartOnFail))
+ if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail))
Restart();
return true;
@@ -921,17 +922,17 @@ public override void OnEntering(IScreen last)
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
- foreach (var mod in Mods.Value.OfType())
+ foreach (var mod in GameplayState.Mods.OfType())
mod.ApplyToPlayer(this);
- foreach (var mod in Mods.Value.OfType())
+ foreach (var mod in GameplayState.Mods.OfType())
mod.ApplyToHUD(HUDOverlay);
// Our mods are local copies of the global mods so they need to be re-applied to the track.
// This is done through the music controller (for now), because resetting speed adjustments on the beatmap track also removes adjustments provided by DrawableTrack.
// Todo: In the future, player will receive in a track and will probably not have to worry about this...
musicController.ResetTrackAdjustments();
- foreach (var mod in Mods.Value.OfType())
+ foreach (var mod in GameplayState.Mods.OfType())
mod.ApplyToTrack(musicController.CurrentTrack);
updateGameplayState();
@@ -954,7 +955,7 @@ protected virtual void StartGameplay()
public override void OnSuspending(IScreen next)
{
- screenSuspension?.Expire();
+ screenSuspension?.RemoveAndDisposeImmediately();
fadeOut();
base.OnSuspending(next);
@@ -962,7 +963,8 @@ public override void OnSuspending(IScreen next)
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 45fe90573b68..853304b7191a 100755
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -15,6 +15,7 @@
using osu.Framework.Input;
using osu.Framework.Screens;
using osu.Framework.Threading;
+using osu.Game.Audio.Effects;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -37,6 +38,8 @@ public class PlayerLoader : ScreenWithBeatmapBackground
protected const float BACKGROUND_BLUR = 15;
+ private const double content_out_duration = 300;
+
public override bool HideOverlaysOnEnter => hideOverlays;
public override bool DisallowExternalBeatmapRulesetChanges => true;
@@ -66,6 +69,8 @@ public class PlayerLoader : ScreenWithBeatmapBackground
private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
+ private AudioFilter lowPassFilter;
+
protected bool BackgroundBrightnessReduction
{
set
@@ -130,41 +135,45 @@ public PlayerLoader(Func createPlayer)
}
[BackgroundDependencyLoader]
- private void load(SessionStatics sessionStatics, MConfigManager config)
+ private void load(SessionStatics sessionStatics, MConfigManager config, AudioManager audio)
{
muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce);
config.BindWith(MSetting.OptUI, optui);
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.001f,
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)
+ };
optui.BindValueChanged(_ => updateExtraDelay());
@@ -249,14 +258,16 @@ public override void OnSuspending(IScreen next)
// stop the track before removing adjustment to avoid a volume spike.
Beatmap.Value.Track.Stop();
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
}
public override bool OnExiting(IScreen next)
{
cancelLoad();
+ contentOut();
- content.ScaleTo(0.7f, 150, Easing.InQuint);
- this.FadeOut(150);
+ // Ensure the screen doesn't expire until all the outwards fade operations have completed.
+ this.Delay(content_out_duration).FadeOut();
ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
@@ -357,6 +368,7 @@ private void contentIn()
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));
}
@@ -366,8 +378,9 @@ private void contentOut()
// 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()
@@ -394,7 +407,7 @@ private void pushWhenLoaded()
this.Delay(extraDelay).Schedule(contentOut);
- TransformSequence pushSequence = this.Delay(250 + extraDelay);
+ TransformSequence pushSequence = this.Delay(250 + extraDelay + 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).
@@ -413,6 +426,11 @@ private void pushWhenLoaded()
})
.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 8a4acacb249f..26887327cddf 100755
--- a/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs
@@ -14,7 +14,7 @@ private void load(OsuColour colours)
{
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 c8e281195a2a..216e46d429dc 100755
--- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs
@@ -29,7 +29,7 @@ private void load(OsuColour colours)
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 10e131626a73..7cc4e49b18fa 100755
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -426,7 +426,7 @@ private void workingBeatmapChanged(ValueChangedEvent e)
{
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 ba3e2bf6ad1f..a5ed0fc9900f 100644
--- a/osu.Game/Skinning/ISkinSource.cs
+++ b/osu.Game/Skinning/ISkinSource.cs
@@ -12,6 +12,9 @@ namespace osu.Game.Skinning
///
public interface ISkinSource : ISkin
{
+ ///
+ /// Fired whenever a source change occurs, signalling that consumers should re-query as required.
+ ///
event Action SourceChanged;
///
diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs
index a12defe87e0e..0c9a82074f88 100644
--- a/osu.Game/Skinning/LegacyScoreCounter.cs
+++ b/osu.Game/Skinning/LegacyScoreCounter.cs
@@ -16,7 +16,6 @@ public class LegacyScoreCounter : GameplayScoreCounter, ISkinnableDrawable
public bool UsesFixedAnchor { get; set; }
public LegacyScoreCounter()
- : base(6)
{
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
index f5a7788359f6..b8847947390e 100644
--- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
+++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
@@ -58,10 +58,8 @@ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnl
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 @@ protected override void OnSourceChanged()
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 ada6e4b7882a..c8e4c2c7b643 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 @@ public class SkinProvidingContainer : Container, ISkinSource
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 @@ public SkinProvidingContainer([CanBeNull] ISkin skin)
: this()
{
if (skin != null)
- AddSource(skin);
+ SetSources(new[] { skin });
}
///
@@ -168,49 +171,42 @@ public IBindable GetConfig(TLookup lookup)
}
///
- /// 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)));
-
- if (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;
+ 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();
- ///
- /// Clears all skin sources.
- ///
- protected void ResetSources()
- {
- foreach (var i in skinSources.ToArray())
- RemoveSource(i.skin);
+ foreach (var skin in skinSources)
+ {
+ if (skin.skin is ISkinSource source)
+ source.SourceChanged += TriggerSourceChanged;
+ }
+ }
}
///
- /// 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 e7125bb0342a..20c2fcc07591 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 @@ public class SkinnableTargetContainer : SkinReloadableDrawable, ISkinnableTarget
public bool ComponentsLoaded { get; private set; }
+ private CancellationTokenSource cancellationSource;
+
public SkinnableTargetContainer(SkinnableTarget target)
{
Target = target;
@@ -38,6 +41,9 @@ public void Reload()
content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer;
+ cancellationSource?.Cancel();
+ cancellationSource = null;
+
if (content != null)
{
LoadComponentAsync(content, wrapper =>
@@ -45,7 +51,7 @@ public void Reload()
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 000000000000..f7b747163439
--- /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 000000000000..27eb5d797fb9
--- /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 64f1ee4a7a2a..6d63525011de 100644
--- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
+++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
@@ -14,6 +14,7 @@
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 @@ private ConvertResult convert(string name, Mod[] mods)
{
var beatmap = GetBeatmap(name);
+ string beforeConversion = beatmap.Serialize();
+
var converterResult = new Dictionary>();
var working = new ConversionWorkingBeatmap(beatmap)
@@ -122,6 +125,10 @@ private ConvertResult convert(string name, Mod[] mods)
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 34393fba7db9..c2e9892735a6 100644
--- a/osu.Game/Tests/Visual/EditorClockTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs
@@ -5,7 +5,6 @@
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 @@ public abstract class EditorClockTestScene : OsuManualInputManagerTestScene
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 @@ private void load()
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 2c0ca0b872da..5e4e5942d93d 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -53,7 +53,8 @@ public TestMultiplayerClient(TestRequestHandlingMultiplayerRoomManager roomManag
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 @@ public MultiplayerRoomUser AddUser(User user, bool markAsPlaying = false)
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 c025cf85c73c..6a11bd3feac2 100644
--- a/osu.Game/Tests/Visual/OsuGameTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs
@@ -22,6 +22,7 @@
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osuTK.Graphics;
+using IntroSequence = osu.Game.Configuration.IntroSequence;
namespace osu.Game.Tests.Visual
{
@@ -77,9 +78,11 @@ public void TearDownSteps()
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;
@@ -126,14 +129,16 @@ public class TestOsuGame : OsuGame
public new Bindable> SelectedMods => base.SelectedMods;
- // if we don't do this, when running under nUnit the version that gets populated is that of nUnit.
+ // if we don't apply these changes, when running under nUnit the version that gets populated is that of nUnit.
+ public override Version AssemblyVersion => new Version(0, 0);
public override string Version => "test game";
protected override Loader CreateLoader() => new TestLoader();
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;
@@ -142,6 +147,9 @@ public TestOsuGame(Storage storage, IAPIProvider api)
protected override void LoadComplete()
{
base.LoadComplete();
+
+ LocalConfig.SetValue(OsuSetting.IntroSequence, IntroSequence.Circles);
+
API.Login("Rhythm Champion", "osu!");
Dependencies.Get().SetValue(Static.MutedAudioNotificationShownOnce, true);
diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
index 42cf826bd476..b2f5b1754f3e 100644
--- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
+++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
@@ -5,6 +5,7 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -24,24 +25,25 @@ protected PlacementBlueprintTestScene()
base.Content.Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock())));
}
- [BackgroundDependencyLoader]
- private void load()
- {
- Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = 2;
- }
-
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs(new EditorClock());
- var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
+ var playable = GetPlayableBeatmap();
dependencies.CacheAs(new EditorBeatmap(playable));
return dependencies;
}
+ protected virtual IBeatmap GetPlayableBeatmap()
+ {
+ var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
+ playable.Difficulty.CircleSize = 2;
+ return playable;
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index cfa2d2632906..cc2608a9d55a 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 edce9d27fecd..92abab036ae3 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,8 +70,8 @@
-
-
+
+
@@ -93,7 +93,7 @@
-
+