diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
index e0ccd50989..358cbda17a 100644
--- a/.github/workflows/report-nunit.yml
+++ b/.github/workflows/report-nunit.yml
@@ -30,3 +30,5 @@ jobs:
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
path: "*.trx"
reporter: dotnet-trx
+ list-suites: 'failed'
+ list-tests: 'failed'
diff --git a/osu.Android.props b/osu.Android.props
index db62667fc2..f552aff2f2 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 0d6925a83d..6d5a960f06 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -42,9 +42,8 @@ namespace osu.Game.Rulesets.Catch.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate;
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index 538a51db5f..5ccb191a9b 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
@@ -13,6 +13,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@@ -101,27 +102,27 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
throw new System.NotImplementedException();
}
- public override float GetBeatSnapDistanceAt(double referenceTime)
+ public override float GetBeatSnapDistanceAt(HitObject referenceObject)
{
throw new System.NotImplementedException();
}
- public override float DurationToDistance(double referenceTime, double duration)
+ public override float DurationToDistance(HitObject referenceObject, double duration)
{
throw new System.NotImplementedException();
}
- public override double DistanceToDuration(double referenceTime, float distance)
+ public override double DistanceToDuration(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
- public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
+ public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
- public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
+ public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 471dad87d5..4387bc6b3b 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -388,7 +388,7 @@ namespace osu.Game.Rulesets.Mania.Tests
},
};
- beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+ beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
}
AddStep("load player", () =>
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
index 18891f8c58..89e13acad6 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 380efff69f..1ed045f7e0 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
- DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
+ DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
double beatLength;
#pragma warning disable 618
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
- beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
+ beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
SpanCount = repeatsData?.SpanCount() ?? 1;
StartTime = (int)Math.Round(hitObject.StartTime);
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
index 7c75b45e5f..ca9e5b0b85 100644
--- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
@@ -13,6 +13,7 @@ SliderTickRate:1
[TimingPoints]
0,500,4,1,0,100,1,0
+10000,-150,4,1,0,100,1,0
[HitObjects]
51,192,500,128,0,1500:1:0:0:0:
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index 3b7da8d9ba..28e970f397 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Mania.UI
// For non-mania beatmap, speed changes should only happen through timing points
if (!isForCurrentRuleset)
- p.DifficultyPoint = new DifficultyControlPoint();
+ p.EffectPoint = new EffectControlPoint();
}
BarLines.ForEach(Playfield.Add);
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
index 851be2b2f2..ef43c3a696 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
@@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
@@ -179,15 +180,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
- public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
+ public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length;
- public float DurationToDistance(double referenceTime, double duration) => (float)duration;
+ public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
- public double DistanceToDuration(double referenceTime, float distance) => distance;
+ public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
- public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
+ public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
- public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
+ public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index f09aad8b49..1f01ba601b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
config.SetValue(OsuSetting.AutoCursorSize, true);
gameplayState.Beatmap.Difficulty.CircleSize = val;
- Scheduler.AddOnce(() => loadContent(false));
+ Scheduler.AddOnce(loadContent);
});
AddStep("test cursor container", () => loadContent(false));
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize);
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
- AddStep("load content", () => loadContent());
+ AddStep("load content", loadContent);
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
@@ -98,7 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
}
- private void loadContent(bool automated = true, Func skinProvider = null)
+ private void loadContent() => loadContent(false);
+
+ private void loadContent(bool automated, Func skinProvider = null)
{
SetContents(_ =>
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
index ececfb0586..d31e7a31f5 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
@@ -407,8 +407,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
-
SelectedMods.Value = new[] { new OsuModClassic() };
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
@@ -439,6 +437,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
+ DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
+
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index 81902c25af..03b4254eed 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -13,6 +13,7 @@ using osuTK.Graphics;
using osu.Game.Rulesets.Mods;
using System.Linq;
using NUnit.Framework;
+using osu.Game.Beatmaps.Legacy;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
@@ -328,10 +329,14 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
{
- var cpi = new ControlPointInfo();
- cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
+ var cpi = new LegacyControlPointInfo();
+ cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
- slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty
+ {
+ CircleSize = circleSize,
+ SliderTickRate = 3
+ });
var drawable = CreateDrawableSlider(slider);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index 590d159300..f3392724ec 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -348,6 +348,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
+ DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f },
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
@@ -362,8 +363,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
-
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
index 1b85e0efde..2d43e1b95e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
@@ -369,8 +369,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
-
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
@@ -399,6 +397,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
+ DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
+
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
index a2fc4848af..d82186fb52 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
@@ -11,6 +11,7 @@ using System.Linq;
using System.Threading;
using osu.Game.Rulesets.Osu.UI;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Beatmaps.Legacy;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in 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;
@@ -89,14 +93,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
- // Don't need to jump to reach spinners
- if (!(BaseObject is Spinner))
- {
- JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
- MovementDistance = Math.Min(JumpDistance, MovementDistance);
- }
+ JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
+ MovementDistance = Math.Min(JumpDistance, MovementDistance);
- if (lastLastObject != null)
+ if (lastLastObject != null && !(lastLastObject is Spinner))
{
Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastObject);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index b9e4ed6fcb..07b6a1bdc2 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -8,12 +8,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
@@ -67,6 +69,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
inputManager = GetContainingInputManager();
}
+ [Resolved]
+ private EditorBeatmap editorBeatmap { get; set; }
+
public override void UpdateTimeAndPosition(SnapResult result)
{
base.UpdateTimeAndPosition(result);
@@ -75,6 +80,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
case SliderPlacementState.Initial:
BeginPlacement();
+
+ var nearestDifficultyPoint = editorBeatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
+
+ HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
break;
@@ -212,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
- HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
+ HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 89724876fa..a7fadfb67f 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePath()
{
- HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
+ HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
editorBeatmap?.Update(HitObject);
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
index ff3be97427..8a561f962a 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
- : base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime)
+ : base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime)
{
Masking = true;
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
new file mode 100644
index 0000000000..c48cbd9992
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
@@ -0,0 +1,76 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.Mods;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Bindables;
+using osu.Framework.Localisation;
+using osu.Framework.Utils;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Configuration;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModNoScope : Mod, IUpdatableByPlayfield, IApplicableToScoreProcessor
+ {
+ ///
+ /// Slightly higher than the cutoff for .
+ ///
+ private const float min_alpha = 0.0002f;
+
+ private const float transition_duration = 100;
+
+ public override string Name => "No Scope";
+ public override string Acronym => "NS";
+ public override ModType Type => ModType.Fun;
+ public override IconUsage? Icon => FontAwesome.Solid.EyeSlash;
+ public override string Description => "Where's the cursor?";
+ public override double ScoreMultiplier => 1;
+
+ private BindableNumber currentCombo;
+
+ private float targetAlpha;
+
+ [SettingSource(
+ "Hidden at combo",
+ "The combo count at which the cursor becomes completely hidden",
+ SettingControlType = typeof(SettingsSlider)
+ )]
+ public BindableInt HiddenComboCount { get; } = new BindableInt
+ {
+ Default = 10,
+ Value = 10,
+ MinValue = 0,
+ MaxValue = 50,
+ };
+
+ public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
+
+ public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
+ {
+ if (HiddenComboCount.Value == 0) return;
+
+ currentCombo = scoreProcessor.Combo.GetBoundCopy();
+ currentCombo.BindValueChanged(combo =>
+ {
+ targetAlpha = Math.Max(min_alpha, 1 - (float)combo.NewValue / HiddenComboCount.Value);
+ }, true);
+ }
+
+ public virtual void Update(Playfield playfield)
+ {
+ playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / transition_duration, 0, 1));
+ }
+ }
+
+ public class HiddenComboSlider : OsuSliderBar
+ {
+ public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 1d494c2917..9e9c75cf31 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -146,9 +146,8 @@ namespace osu.Game.Rulesets.Osu.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
@@ -181,7 +180,6 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
- SampleControlPoint = SampleControlPoint,
});
break;
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index f4a93a571d..ee4712c3b8 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -192,6 +192,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModBarrelRoll(),
new OsuModApproachDifferent(),
new OsuModMuted(),
+ new OsuModNoScope(),
};
case ModType.System:
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 50c0ca7f55..32aad6c36a 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
- DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
+ DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint;
double beatLength;
#pragma warning disable 618
@@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
- beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
+ beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index 0318e32991..0e93ad7e73 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -63,9 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = base_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
tickSpacing = timingPoint.BeatLength / TickRate;
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index b5e1fa204f..cb12d03620 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -192,15 +192,15 @@ namespace osu.Game.Tests.Beatmaps.Formats
var difficultyPoint = controlPoints.DifficultyPointAt(0);
Assert.AreEqual(0, difficultyPoint.Time);
- Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
+ Assert.AreEqual(1.0, difficultyPoint.SliderVelocity);
difficultyPoint = controlPoints.DifficultyPointAt(48428);
Assert.AreEqual(0, difficultyPoint.Time);
- Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
+ Assert.AreEqual(1.0, difficultyPoint.SliderVelocity);
difficultyPoint = controlPoints.DifficultyPointAt(116999);
Assert.AreEqual(116999, difficultyPoint.Time);
- Assert.AreEqual(0.75, difficultyPoint.SpeedMultiplier, 0.1);
+ Assert.AreEqual(0.75, difficultyPoint.SliderVelocity, 0.1);
var soundPoint = controlPoints.SamplePointAt(0);
Assert.AreEqual(956, soundPoint.Time);
@@ -227,7 +227,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(effectPoint.KiaiMode);
Assert.IsFalse(effectPoint.OmitFirstBarLine);
- effectPoint = controlPoints.EffectPointAt(119637);
+ effectPoint = controlPoints.EffectPointAt(116637);
Assert.AreEqual(95901, effectPoint.Time);
Assert.IsFalse(effectPoint.KiaiMode);
Assert.IsFalse(effectPoint.OmitFirstBarLine);
@@ -249,10 +249,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(3));
Assert.That(controlPoints.SamplePoints.Count, Is.EqualTo(3));
- Assert.That(controlPoints.DifficultyPointAt(500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(1500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(2500).SpeedMultiplier, Is.EqualTo(0.75).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(3500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(1500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(2500).SliderVelocity, Is.EqualTo(0.75).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(3500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
Assert.That(controlPoints.EffectPointAt(500).KiaiMode, Is.True);
Assert.That(controlPoints.EffectPointAt(1500).KiaiMode, Is.True);
@@ -279,10 +279,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("timingpoint-speedmultiplier-reset.osu"))
using (var stream = new LineBufferedReader(resStream))
{
- var controlPoints = decoder.Decode(stream).ControlPointInfo;
+ var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
- Assert.That(controlPoints.DifficultyPointAt(0).SpeedMultiplier, Is.EqualTo(0.5).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(0).SliderVelocity, Is.EqualTo(0.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1).Within(0.1));
}
}
@@ -394,12 +394,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("controlpoint-difficulty-multiplier.osu"))
using (var stream = new LineBufferedReader(resStream))
{
- var controlPointInfo = decoder.Decode(stream).ControlPointInfo;
+ var controlPointInfo = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
- Assert.That(controlPointInfo.DifficultyPointAt(5).SpeedMultiplier, Is.EqualTo(1));
- Assert.That(controlPointInfo.DifficultyPointAt(1000).SpeedMultiplier, Is.EqualTo(10));
- Assert.That(controlPointInfo.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1.8518518518518519d));
- Assert.That(controlPointInfo.DifficultyPointAt(3000).SpeedMultiplier, Is.EqualTo(0.5));
+ Assert.That(controlPointInfo.DifficultyPointAt(5).SliderVelocity, Is.EqualTo(1));
+ Assert.That(controlPointInfo.DifficultyPointAt(1000).SliderVelocity, Is.EqualTo(10));
+ Assert.That(controlPointInfo.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1.8518518518518519d));
+ Assert.That(controlPointInfo.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(0.5));
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index 896aa53f82..d12da1a22f 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -46,8 +46,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
sort(decoded.beatmap);
sort(decodedAfterEncode.beatmap);
- Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
- Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
+ compareBeatmaps(decoded, decodedAfterEncode);
}
[TestCaseSource(nameof(allBeatmaps))]
@@ -62,8 +61,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
sort(decoded.beatmap);
sort(decodedAfterEncode.beatmap);
- Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
- Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
+ compareBeatmaps(decoded, decodedAfterEncode);
}
[TestCaseSource(nameof(allBeatmaps))]
@@ -77,12 +75,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
- // in this process, we may lose some detail in the control points section.
- // let's focus on only the hitobjects.
- var originalHitObjects = decoded.beatmap.HitObjects.Serialize();
- var newHitObjects = decodedAfterEncode.beatmap.HitObjects.Serialize();
-
- Assert.That(newHitObjects, Is.EqualTo(originalHitObjects));
+ compareBeatmaps(decoded, decodedAfterEncode);
ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
{
@@ -97,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
// completely ignore "legacy" types, which have been moved to HitObjects.
// even though these would mostly be ignored by the Add call, they will still be available in groups,
// which isn't what we want to be testing here.
- if (point is SampleControlPoint)
+ if (point is SampleControlPoint || point is DifficultyControlPoint)
continue;
newControlPoints.Add(point.Time, point.DeepClone());
@@ -107,6 +100,19 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ private void compareBeatmaps((IBeatmap beatmap, TestLegacySkin skin) expected, (IBeatmap beatmap, TestLegacySkin skin) actual)
+ {
+ // Check all control points that are still considered to be at a global level.
+ Assert.That(expected.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.TimingPoints.Serialize()));
+ Assert.That(expected.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.EffectPoints.Serialize()));
+
+ // Check all hitobjects.
+ Assert.That(expected.beatmap.HitObjects.Serialize(), Is.EqualTo(actual.beatmap.HitObjects.Serialize()));
+
+ // Check skin.
+ Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration));
+ }
+
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{
@@ -156,7 +162,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
- private (IBeatmap beatmap, TestLegacySkin beatmapSkin) decodeFromLegacy(Stream stream, string name)
+ private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name)
{
using (var reader = new LineBufferedReader(stream))
{
@@ -174,7 +180,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
- private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
+ private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin skin) fullBeatmap)
{
var (beatmap, beatmapSkin) = fullBeatmap;
var stream = new MemoryStream();
diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs
new file mode 100644
index 0000000000..f4e0838be1
--- /dev/null
+++ b/osu.Game.Tests/Database/RulesetStoreTests.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Models;
+using osu.Game.Stores;
+
+namespace osu.Game.Tests.Database
+{
+ public class RulesetStoreTests : RealmTest
+ {
+ [Test]
+ public void TestCreateStore()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var rulesets = new RealmRulesetStore(realmFactory, storage);
+
+ Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
+ Assert.AreEqual(4, realmFactory.Context.All().Count());
+ });
+ }
+
+ [Test]
+ public void TestCreateStoreTwiceDoesntAddRulesetsAgain()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var rulesets = new RealmRulesetStore(realmFactory, storage);
+ var rulesets2 = new RealmRulesetStore(realmFactory, storage);
+
+ Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
+ Assert.AreEqual(4, rulesets2.AvailableRulesets.Count());
+
+ Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First());
+ Assert.AreEqual(4, realmFactory.Context.All().Count());
+ });
+ }
+
+ [Test]
+ public void TestRetrievedRulesetsAreDetached()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var rulesets = new RealmRulesetStore(realmFactory, storage);
+
+ Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false);
+ Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false);
+ Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
new file mode 100644
index 0000000000..f3a4f10210
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
@@ -0,0 +1,104 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Storyboards;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckAudioInVideoTest
+ {
+ private CheckAudioInVideo check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckAudioInVideo();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.mp4",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestRegularVideoFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4"))
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+
+ [Test]
+ public void TestVideoFileWithAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-audio.mp4"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
+ }
+ }
+
+ [Test]
+ public void TestVideoFileWithTrackButNoAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-track-but-no-audio.mp4"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
+ }
+ }
+
+ [Test]
+ public void TestMissingFile()
+ {
+ beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
+
+ var issues = check.Run(getContext(null)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateMissingFile);
+ }
+
+ private BeatmapVerifierContext getContext(Stream resourceStream)
+ {
+ var storyboard = new Storyboard();
+ var layer = storyboard.GetLayer("Video");
+ layer.Add(new StoryboardVideo("abc123.mp4", 0));
+
+ var mockWorkingBeatmap = new Mock(beatmap, null, null);
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream);
+ mockWorkingBeatmap.As().SetupGet(w => w.Storyboard).Returns(storyboard);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
new file mode 100644
index 0000000000..9b090591bc
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
@@ -0,0 +1,128 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using ManagedBass;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+using osuTK.Audio;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckTooShortAudioFilesTest
+ {
+ private CheckTooShortAudioFiles check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckTooShortAudioFiles();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.wav",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+
+ // 0 = No output device. This still allows decoding.
+ if (!Bass.Init(0) && Bass.LastError != Errors.Already)
+ throw new AudioException("Could not initialize Bass.");
+ }
+
+ [Test]
+ public void TestDifferentExtension()
+ {
+ beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
+ beatmap.BeatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo
+ {
+ Filename = "abc123.jpg",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ });
+
+ // Should fail to load, but not produce an error due to the extension not being expected to load.
+ Assert.IsEmpty(check.Run(getContext(null, allowMissing: true)));
+ }
+
+ [Test]
+ public void TestRegularAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample.mp3"))
+ {
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+ }
+
+ [Test]
+ public void TestBlankAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/blank.wav"))
+ {
+ // This is a 0 ms duration audio file, commonly used to silence sliderslides/ticks, and so should be fine.
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+ }
+
+ [Test]
+ public void TestTooShortAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateTooShort);
+ }
+ }
+
+ [Test]
+ public void TestMissingAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3"))
+ {
+ Assert.IsEmpty(check.Run(getContext(resourceStream, allowMissing: true)));
+ }
+ }
+
+ [Test]
+ public void TestCorruptAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat);
+ }
+ }
+
+ private BeatmapVerifierContext getContext(Stream resourceStream, bool allowMissing = false)
+ {
+ var mockWorkingBeatmap = new Mock(beatmap, null, null);
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs
new file mode 100644
index 0000000000..c9adc030c1
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs
@@ -0,0 +1,86 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckZeroByteFilesTest
+ {
+ private CheckZeroByteFiles check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckZeroByteFiles();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.jpg",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestNonZeroBytes()
+ {
+ Assert.IsEmpty(check.Run(getContext(byteLength: 44)));
+ }
+
+ [Test]
+ public void TestZeroBytes()
+ {
+ var issues = check.Run(getContext(byteLength: 0)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckZeroByteFiles.IssueTemplateZeroBytes);
+ }
+
+ [Test]
+ public void TestMissing()
+ {
+ Assert.IsEmpty(check.Run(getContextMissing()));
+ }
+
+ private BeatmapVerifierContext getContext(long byteLength)
+ {
+ var mockStream = new Mock();
+ mockStream.Setup(s => s.Length).Returns(byteLength);
+
+ var mockWorkingBeatmap = new Mock();
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(mockStream.Object);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+
+ private BeatmapVerifierContext getContextMissing()
+ {
+ var mockWorkingBeatmap = new Mock();
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns((Stream)null);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index a40a6dac4c..8eb9452736 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
@@ -55,8 +56,6 @@ namespace osu.Game.Tests.Editing
composer.EditorBeatmap.Difficulty.SliderMultiplier = 1;
composer.EditorBeatmap.ControlPointInfo.Clear();
-
- composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 });
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 });
});
@@ -73,13 +72,13 @@ namespace osu.Game.Tests.Editing
[TestCase(2)]
public void TestSpeedMultiplier(float multiplier)
{
- AddStep($"set multiplier = {multiplier}", () =>
+ assertSnapDistance(100 * multiplier, new HitObject
{
- composer.EditorBeatmap.ControlPointInfo.Clear();
- composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = multiplier });
+ DifficultyControlPoint = new DifficultyControlPoint
+ {
+ SliderVelocity = multiplier
+ }
});
-
- assertSnapDistance(100 * multiplier);
}
[TestCase(1)]
@@ -197,20 +196,20 @@ namespace osu.Game.Tests.Editing
assertSnappedDistance(400, 400);
}
- private void assertSnapDistance(float expectedDistance)
- => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(0) == expectedDistance);
+ private void assertSnapDistance(float expectedDistance, HitObject hitObject = null)
+ => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()) == expectedDistance);
private void assertDurationToDistance(double duration, float expectedDistance)
- => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(0, duration) == expectedDistance);
+ => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance);
private void assertDistanceToDuration(float distance, double expectedDuration)
- => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(0, distance) == expectedDuration);
+ => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration);
private void assertSnappedDuration(float distance, double expectedDuration)
- => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(0, distance) == expectedDuration);
+ => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(new HitObject(), distance) == expectedDuration);
private void assertSnappedDistance(float distance, float expectedDistance)
- => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(0, distance) == expectedDistance);
+ => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(new HitObject(), distance) == expectedDistance);
private class TestHitObjectComposer : OsuHitObjectComposer
{
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
index fabb016d5f..cfda4f6422 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddRedundantDifficulty()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
cpi.Add(0, new DifficultyControlPoint()); // is redundant
cpi.Add(1000, new DifficultyControlPoint()); // is redundant
@@ -55,7 +55,7 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0));
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
- cpi.Add(1000, new DifficultyControlPoint { SpeedMultiplier = 2 }); // is not redundant
+ cpi.Add(1000, new DifficultyControlPoint { SliderVelocity = 2 }); // is not redundant
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
@@ -159,7 +159,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddControlPointToGroup()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
@@ -174,23 +174,23 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddDuplicateControlPointToGroup()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
group.Add(new DifficultyControlPoint());
- group.Add(new DifficultyControlPoint { SpeedMultiplier = 2 });
+ group.Add(new DifficultyControlPoint { SliderVelocity = 2 });
Assert.That(group.ControlPoints.Count, Is.EqualTo(1));
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
- Assert.That(cpi.DifficultyPoints.First().SpeedMultiplier, Is.EqualTo(2));
+ Assert.That(cpi.DifficultyPoints.First().SliderVelocity, Is.EqualTo(2));
}
[Test]
public void TestRemoveControlPointFromGroup()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
@@ -208,14 +208,14 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestOrdering()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
cpi.Add(0, new TimingControlPoint());
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
- cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
- cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
+ cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 });
+ cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 });
cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
@@ -230,14 +230,14 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestClear()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
cpi.Add(0, new TimingControlPoint());
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
- cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
- cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
+ cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 });
+ cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 });
cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
diff --git a/osu.Game.Tests/Resources/Samples/blank.wav b/osu.Game.Tests/Resources/Samples/blank.wav
new file mode 100644
index 0000000000..878bf23cea
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/blank.wav differ
diff --git a/osu.Game.Tests/Resources/Samples/corrupt.wav b/osu.Game.Tests/Resources/Samples/corrupt.wav
new file mode 100644
index 0000000000..87c7de4b7b
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/corrupt.wav differ
diff --git a/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3
new file mode 100644
index 0000000000..003fe23dca
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4
new file mode 100644
index 0000000000..5d380ab50c
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4
new file mode 100644
index 0000000000..7cdd1939e9
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video.mp4 b/osu.Game.Tests/Resources/Videos/test-video.mp4
new file mode 100644
index 0000000000..795483c096
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video.mp4 differ
diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
index 0107632f6e..99be72e958 100644
--- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
+++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
@@ -163,5 +163,11 @@ namespace osu.Game.Tests.Visual.Audio
}
private void waitTrackPlay() => AddWaitStep("Let track play", 10);
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ track?.Dispose();
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
index 11830ebe35..d1efd22d6f 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
@@ -81,7 +82,7 @@ namespace osu.Game.Tests.Visual.Editing
public new float DistanceSpacing => base.DistanceSpacing;
public TestDistanceSnapGrid(double? endTime = null)
- : base(grid_position, 0, endTime)
+ : base(new HitObject(), grid_position, 0, endTime)
{
}
@@ -158,15 +159,15 @@ namespace osu.Game.Tests.Visual.Editing
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
- public float GetBeatSnapDistanceAt(double referenceTime) => 10;
+ public float GetBeatSnapDistanceAt(HitObject referenceObject) => 10;
- public float DurationToDistance(double referenceTime, double duration) => (float)duration;
+ public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
- public double DistanceToDuration(double referenceTime, float distance) => distance;
+ public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
- public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
+ public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
- public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
+ public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
}
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
index f0aa3e2350..ab2bc4649a 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -8,6 +8,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osuTK.Input;
@@ -30,23 +31,35 @@ namespace osu.Game.Tests.Visual.Editing
PushAndConfirm(() => new EditorLoader());
- AddUntilStep("wait for editor load", () => editor != null);
+ AddUntilStep("wait for editor load", () => editor?.IsLoaded == true);
- AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7);
+ AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
- AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
+ // 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));
- AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
+ checkMutations();
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
+ checkMutations();
+
AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@@ -58,8 +71,16 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
AddUntilStep("Wait for editor load", () => editor != null);
+
+ checkMutations();
+ }
+
+ private void checkMutations()
+ {
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7);
+ AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
+ AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.Version == "difficulty");
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 2f15e549f7..283fe594ea 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -93,9 +93,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private IList testControlPoints => new List
{
- new MultiplierControlPoint(time_range) { DifficultyPoint = { SpeedMultiplier = 1.25 } },
- new MultiplierControlPoint(1.5 * time_range) { DifficultyPoint = { SpeedMultiplier = 1 } },
- new MultiplierControlPoint(2 * time_range) { DifficultyPoint = { SpeedMultiplier = 1.5 } }
+ new MultiplierControlPoint(time_range) { EffectPoint = { ScrollSpeed = 1.25 } },
+ new MultiplierControlPoint(1.5 * time_range) { EffectPoint = { ScrollSpeed = 1 } },
+ new MultiplierControlPoint(2 * time_range) { EffectPoint = { ScrollSpeed = 1.5 } }
};
[Test]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 0b70703870..2bb77395ef 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -564,11 +564,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddRepeatStep("click spectate button", () =>
+ AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("click ready button", () =>
{
- InputManager.MoveMouseTo(this.ChildrenOfType().Single());
+ InputManager.MoveMouseTo(readyButton);
InputManager.Click(MouseButton.Left);
- }, 2);
+ });
+
+ AddUntilStep("wait for player to be ready", () => client.Room?.Users[0].State == MultiplayerUserState.Ready);
+ AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("click start button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
@@ -582,6 +589,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for results", () => Stack.CurrentScreen is ResultsScreen);
}
+ private MultiplayerReadyButton readyButton => this.ChildrenOfType().Single();
+
private void createRoom(Func room)
{
AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
index cb7c334656..bd723eeed6 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
@@ -4,7 +4,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
-using osu.Game.Database;
+using osu.Game.Overlays.Notifications;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Navigation
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestImportCreatedNotification()
{
- AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType().Count() == 1);
+ AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType().Count() == 1);
}
}
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
index a5b90e6655..0ae4e0c5dc 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
@@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
- () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)));
+ () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItems().ElementAt(index).ChildrenOfType().Single();
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
index 393420e700..1b7f65f9a0 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
@@ -1,11 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -19,28 +23,62 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("create component", () =>
{
- LabelledSliderBar component;
+ FillFlowContainer flow;
- Child = new Container
+ Child = flow = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
- Child = component = new LabelledSliderBar
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- Current = new BindableDouble(5)
+ new LabelledSliderBar
{
- MinValue = 0,
- MaxValue = 10,
- Precision = 1,
- }
- }
+ Current = new BindableDouble(5)
+ {
+ MinValue = 0,
+ MaxValue = 10,
+ Precision = 1,
+ },
+ Label = "a sample component",
+ Description = hasDescription ? "this text describes the component" : string.Empty,
+ },
+ },
};
- component.Label = "a sample component";
- component.Description = hasDescription ? "this text describes the component" : string.Empty;
+ foreach (var colour in Enum.GetValues(typeof(OverlayColourScheme)).OfType())
+ {
+ flow.Add(new OverlayColourContainer(colour)
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = new LabelledSliderBar
+ {
+ Current = new BindableDouble(5)
+ {
+ MinValue = 0,
+ MaxValue = 10,
+ Precision = 1,
+ },
+ Label = "a sample component",
+ Description = hasDescription ? "this text describes the component" : string.Empty,
+ }
+ });
+ }
});
}
+
+ private class OverlayColourContainer : Container
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider;
+
+ public OverlayColourContainer(OverlayColourScheme scheme)
+ {
+ colourProvider = new OverlayColourProvider(scheme);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs
new file mode 100644
index 0000000000..fb04c5bad0
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs
@@ -0,0 +1,68 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Settings;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneSettingsCheckbox : OsuTestScene
+ {
+ [TestCase]
+ public void TestCheckbox()
+ {
+ AddStep("create component", () =>
+ {
+ FillFlowContainer flow;
+
+ Child = flow = new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 500,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(5),
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new SettingsCheckbox
+ {
+ LabelText = "a sample component",
+ },
+ },
+ };
+
+ foreach (var colour1 in Enum.GetValues(typeof(OverlayColourScheme)).OfType())
+ {
+ flow.Add(new OverlayColourContainer(colour1)
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = new SettingsCheckbox
+ {
+ LabelText = "a sample component",
+ }
+ });
+ }
+ });
+ }
+
+ private class OverlayColourContainer : Container
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider;
+
+ public OverlayColourContainer(OverlayColourScheme scheme)
+ {
+ colourProvider = new OverlayColourProvider(scheme);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 14175f251b..562cbfabf0 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -176,11 +176,6 @@ namespace osu.Game.Beatmaps
}
}
- ///
- /// Fired when the user requests to view the resulting import.
- ///
- public Action>> PresentImport { set => beatmapModelManager.PostImport = value; }
-
///
/// Delete a beatmap difficulty.
///
@@ -338,5 +333,14 @@ namespace osu.Game.Beatmaps
}
#endregion
+
+ #region Implementation of IPostImports
+
+ public Action>> PostImport
+ {
+ set => beatmapModelManager.PostImport = value;
+ }
+
+ #endregion
}
}
diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs
index 9c0fc5ef8a..76019a15ae 100644
--- a/osu.Game/Beatmaps/BeatmapModelManager.cs
+++ b/osu.Game/Beatmaps/BeatmapModelManager.cs
@@ -192,6 +192,13 @@ namespace osu.Game.Beatmaps
{
var setInfo = beatmapInfo.BeatmapSet;
+ // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
+ // This should hopefully be temporary, assuming said clone is eventually removed.
+ beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty);
+
+ // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
+ beatmapContent.BeatmapInfo = beatmapInfo;
+
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
@@ -202,7 +209,6 @@ namespace osu.Game.Beatmaps
using (ContextFactory.GetForWrite())
{
beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == beatmapInfo.ID);
- beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty);
var metadata = beatmapInfo.Metadata ?? setInfo.Metadata;
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index 8203f2e968..4079a0cd5f 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -15,11 +15,9 @@ namespace osu.Game.Beatmaps.ControlPoints
/// The time at which the control point takes effect.
///
[JsonIgnore]
- public double Time => controlPointGroup?.Time ?? 0;
+ public double Time { get; set; }
- private ControlPointGroup controlPointGroup;
-
- public void AttachGroup(ControlPointGroup pointGroup) => controlPointGroup = pointGroup;
+ public void AttachGroup(ControlPointGroup pointGroup) => Time = pointGroup.Time;
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
@@ -46,6 +44,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public virtual void CopyFrom(ControlPoint other)
{
+ Time = other.Time;
}
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index 3ff40fe194..9d738ecbfb 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -33,14 +33,6 @@ namespace osu.Game.Beatmaps.ControlPoints
private readonly SortedList timingPoints = new SortedList(Comparer.Default);
- ///
- /// All difficulty points.
- ///
- [JsonProperty]
- public IReadOnlyList DifficultyPoints => difficultyPoints;
-
- private readonly SortedList difficultyPoints = new SortedList(Comparer.Default);
-
///
/// All effect points.
///
@@ -55,14 +47,6 @@ namespace osu.Game.Beatmaps.ControlPoints
[JsonIgnore]
public IEnumerable AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray();
- ///
- /// Finds the difficulty control point that is active at .
- ///
- /// The time to find the difficulty control point at.
- /// The difficulty control point.
- [NotNull]
- public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
-
///
/// Finds the effect control point that is active at .
///
@@ -100,7 +84,6 @@ namespace osu.Game.Beatmaps.ControlPoints
{
groups.Clear();
timingPoints.Clear();
- difficultyPoints.Clear();
effectPoints.Clear();
}
@@ -277,10 +260,6 @@ namespace osu.Game.Beatmaps.ControlPoints
case EffectControlPoint _:
existing = EffectPointAt(time);
break;
-
- case DifficultyControlPoint _:
- existing = DifficultyPointAt(time);
- break;
}
return newPoint?.IsRedundant(existing) == true;
@@ -298,9 +277,8 @@ namespace osu.Game.Beatmaps.ControlPoints
effectPoints.Add(typed);
break;
- case DifficultyControlPoint typed:
- difficultyPoints.Add(typed);
- break;
+ default:
+ throw new ArgumentException($"A control point of unexpected type {controlPoint.GetType()} was added to this {nameof(ControlPointInfo)}");
}
}
@@ -315,10 +293,6 @@ namespace osu.Game.Beatmaps.ControlPoints
case EffectControlPoint typed:
effectPoints.Remove(typed);
break;
-
- case DifficultyControlPoint typed:
- difficultyPoints.Remove(typed);
- break;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 8a6cfaf688..bf7ed8e6f5 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -7,17 +7,20 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
+ ///
+ /// Note that going forward, this control point type should always be assigned directly to HitObjects.
+ ///
public class DifficultyControlPoint : ControlPoint
{
public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint
{
- SpeedMultiplierBindable = { Disabled = true },
+ SliderVelocityBindable = { Disabled = true },
};
///
- /// The speed multiplier at this control point.
+ /// The slider velocity at this control point.
///
- public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1)
+ public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1)
{
Precision = 0.01,
Default = 1,
@@ -28,21 +31,21 @@ namespace osu.Game.Beatmaps.ControlPoints
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
///
- /// The speed multiplier at this control point.
+ /// The slider velocity at this control point.
///
- public double SpeedMultiplier
+ public double SliderVelocity
{
- get => SpeedMultiplierBindable.Value;
- set => SpeedMultiplierBindable.Value = value;
+ get => SliderVelocityBindable.Value;
+ set => SliderVelocityBindable.Value = value;
}
public override bool IsRedundant(ControlPoint existing)
=> existing is DifficultyControlPoint existingDifficulty
- && SpeedMultiplier == existingDifficulty.SpeedMultiplier;
+ && SliderVelocity == existingDifficulty.SliderVelocity;
public override void CopyFrom(ControlPoint other)
{
- SpeedMultiplier = ((DifficultyControlPoint)other).SpeedMultiplier;
+ SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity;
base.CopyFrom(other);
}
diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
index 79bc88e773..7f550a52fc 100644
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -12,7 +12,8 @@ namespace osu.Game.Beatmaps.ControlPoints
public static readonly EffectControlPoint DEFAULT = new EffectControlPoint
{
KiaiModeBindable = { Disabled = true },
- OmitFirstBarLineBindable = { Disabled = true }
+ OmitFirstBarLineBindable = { Disabled = true },
+ ScrollSpeedBindable = { Disabled = true }
};
///
@@ -20,6 +21,26 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public readonly BindableBool OmitFirstBarLineBindable = new BindableBool();
+ ///
+ /// The relative scroll speed at this control point.
+ ///
+ public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1)
+ {
+ Precision = 0.01,
+ Default = 1,
+ MinValue = 0.01,
+ MaxValue = 10
+ };
+
+ ///
+ /// The relative scroll speed.
+ ///
+ public double ScrollSpeed
+ {
+ get => ScrollSpeedBindable.Value;
+ set => ScrollSpeedBindable.Value = value;
+ }
+
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple;
///
@@ -49,12 +70,14 @@ namespace osu.Game.Beatmaps.ControlPoints
=> !OmitFirstBarLine
&& existing is EffectControlPoint existingEffect
&& KiaiMode == existingEffect.KiaiMode
- && OmitFirstBarLine == existingEffect.OmitFirstBarLine;
+ && OmitFirstBarLine == existingEffect.OmitFirstBarLine
+ && ScrollSpeed == existingEffect.ScrollSpeed;
public override void CopyFrom(ControlPoint other)
{
KiaiMode = ((EffectControlPoint)other).KiaiMode;
OmitFirstBarLine = ((EffectControlPoint)other).OmitFirstBarLine;
+ ScrollSpeed = ((EffectControlPoint)other).ScrollSpeed;
base.CopyFrom(other);
}
diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
index 4aa6a3d6e9..fb489f73b1 100644
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -8,6 +8,9 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
+ ///
+ /// Note that going forward, this control point type should always be assigned directly to HitObjects.
+ ///
public class SampleControlPoint : ControlPoint
{
public const string DEFAULT_BANK = "normal";
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index f71b148008..bef2d78f21 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -384,14 +384,21 @@ namespace osu.Game.Beatmaps.Formats
addControlPoint(time, new LegacyDifficultyControlPoint(beatLength)
#pragma warning restore 618
{
- SpeedMultiplier = speedMultiplier,
+ SliderVelocity = speedMultiplier,
}, timingChange);
- addControlPoint(time, new EffectControlPoint
+ var effectPoint = new EffectControlPoint
{
KiaiMode = kiaiMode,
OmitFirstBarLine = omitFirstBarSignature,
- }, timingChange);
+ };
+
+ bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0;
+ // scrolling rulesets use effect points rather than difficulty points for scroll speed adjustments.
+ if (!isOsuRuleset)
+ effectPoint.ScrollSpeed = speedMultiplier;
+
+ addControlPoint(time, effectPoint, timingChange);
addControlPoint(time, new LegacySampleControlPoint
{
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 74b3c178cd..1dc270ee63 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -170,33 +170,30 @@ namespace osu.Game.Beatmaps.Formats
if (beatmap.ControlPointInfo.Groups.Count == 0)
return;
+ var legacyControlPoints = new LegacyControlPointInfo();
+ foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
+ legacyControlPoints.Add(point.Time, point.DeepClone());
+
writer.WriteLine("[TimingPoints]");
- if (!(beatmap.ControlPointInfo is LegacyControlPointInfo))
+ SampleControlPoint lastRelevantSamplePoint = null;
+ DifficultyControlPoint lastRelevantDifficultyPoint = null;
+
+ bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0;
+
+ // iterate over hitobjects and pull out all required sample and difficulty changes
+ extractDifficultyControlPoints(beatmap.HitObjects);
+ extractSampleControlPoints(beatmap.HitObjects);
+
+ // handle scroll speed, which is stored as "slider velocity" in legacy formats.
+ // this is relevant for scrolling ruleset beatmaps.
+ if (!isOsuRuleset)
{
- var legacyControlPoints = new LegacyControlPointInfo();
-
- foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
- legacyControlPoints.Add(point.Time, point.DeepClone());
-
- beatmap.ControlPointInfo = legacyControlPoints;
-
- SampleControlPoint lastRelevantSamplePoint = null;
-
- // iterate over hitobjects and pull out all required sample changes
- foreach (var h in beatmap.HitObjects)
- {
- var hSamplePoint = h.SampleControlPoint;
-
- if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint))
- {
- legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint);
- lastRelevantSamplePoint = hSamplePoint;
- }
- }
+ foreach (var point in legacyControlPoints.EffectPoints)
+ legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed });
}
- foreach (var group in beatmap.ControlPointInfo.Groups)
+ foreach (var group in legacyControlPoints.Groups)
{
var groupTimingPoint = group.ControlPoints.OfType().FirstOrDefault();
@@ -209,16 +206,16 @@ namespace osu.Game.Beatmaps.Formats
}
// Output any remaining effects as secondary non-timing control point.
- var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time);
+ var difficultyPoint = legacyControlPoints.DifficultyPointAt(group.Time);
writer.Write(FormattableString.Invariant($"{group.Time},"));
- writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SpeedMultiplier},"));
+ writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SliderVelocity},"));
outputControlPointAt(group.Time, false);
}
void outputControlPointAt(double time, bool isTimingPoint)
{
- var samplePoint = ((LegacyControlPointInfo)beatmap.ControlPointInfo).SamplePointAt(time);
- var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time);
+ var samplePoint = legacyControlPoints.SamplePointAt(time);
+ var effectPoint = legacyControlPoints.EffectPointAt(time);
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)
HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty));
@@ -230,7 +227,7 @@ namespace osu.Game.Beatmaps.Formats
if (effectPoint.OmitFirstBarLine)
effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
- writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(time).TimeSignature},"));
+ writer.Write(FormattableString.Invariant($"{(int)legacyControlPoints.TimingPointAt(time).TimeSignature},"));
writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},"));
writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},"));
writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},"));
@@ -238,6 +235,55 @@ namespace osu.Game.Beatmaps.Formats
writer.Write(FormattableString.Invariant($"{(int)effectFlags}"));
writer.WriteLine();
}
+
+ IEnumerable collectDifficultyControlPoints(IEnumerable hitObjects)
+ {
+ if (!isOsuRuleset)
+ yield break;
+
+ foreach (var hitObject in hitObjects)
+ {
+ yield return hitObject.DifficultyControlPoint;
+
+ foreach (var nested in collectDifficultyControlPoints(hitObject.NestedHitObjects))
+ yield return nested;
+ }
+ }
+
+ void extractDifficultyControlPoints(IEnumerable hitObjects)
+ {
+ foreach (var hDifficultyPoint in collectDifficultyControlPoints(hitObjects).OrderBy(dp => dp.Time))
+ {
+ if (!hDifficultyPoint.IsRedundant(lastRelevantDifficultyPoint))
+ {
+ legacyControlPoints.Add(hDifficultyPoint.Time, hDifficultyPoint);
+ lastRelevantDifficultyPoint = hDifficultyPoint;
+ }
+ }
+ }
+
+ IEnumerable collectSampleControlPoints(IEnumerable hitObjects)
+ {
+ foreach (var hitObject in hitObjects)
+ {
+ yield return hitObject.SampleControlPoint;
+
+ foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects))
+ yield return nested;
+ }
+ }
+
+ void extractSampleControlPoints(IEnumerable hitObject)
+ {
+ foreach (var hSamplePoint in collectSampleControlPoints(hitObject).OrderBy(sp => sp.Time))
+ {
+ if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint))
+ {
+ legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint);
+ lastRelevantSamplePoint = hSamplePoint;
+ }
+ }
+ }
}
private void handleColours(TextWriter writer)
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 20080308f9..cf6c827af5 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -181,7 +181,7 @@ namespace osu.Game.Beatmaps.Formats
public LegacyDifficultyControlPoint()
{
- SpeedMultiplierBindable.Precision = double.Epsilon;
+ SliderVelocityBindable.Precision = double.Epsilon;
}
public override void CopyFrom(ControlPoint other)
diff --git a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs
index ff0ca5ebe1..2b0a2e7a4d 100644
--- a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs
+++ b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs
@@ -1,9 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
-using osu.Framework.Bindables;
+using osu.Framework.Lists;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Beatmaps.Legacy
@@ -14,9 +15,9 @@ namespace osu.Game.Beatmaps.Legacy
/// All sound points.
///
[JsonProperty]
- public IBindableList SamplePoints => samplePoints;
+ public IReadOnlyList SamplePoints => samplePoints;
- private readonly BindableList samplePoints = new BindableList();
+ private readonly SortedList samplePoints = new SortedList(Comparer.Default);
///
/// Finds the sound control point that is active at .
@@ -26,35 +27,76 @@ namespace osu.Game.Beatmaps.Legacy
[NotNull]
public SampleControlPoint SamplePointAt(double time) => BinarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
+ ///
+ /// All difficulty points.
+ ///
+ [JsonProperty]
+ public IReadOnlyList DifficultyPoints => difficultyPoints;
+
+ private readonly SortedList difficultyPoints = new SortedList(Comparer.Default);
+
+ ///
+ /// Finds the difficulty control point that is active at .
+ ///
+ /// The time to find the difficulty control point at.
+ /// The difficulty control point.
+ [NotNull]
+ public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
+
public override void Clear()
{
base.Clear();
samplePoints.Clear();
+ difficultyPoints.Clear();
}
protected override bool CheckAlreadyExisting(double time, ControlPoint newPoint)
{
- if (newPoint is SampleControlPoint)
+ switch (newPoint)
{
- var existing = BinarySearch(SamplePoints, time);
- return newPoint.IsRedundant(existing);
- }
+ case SampleControlPoint _:
+ // intentionally don't use SamplePointAt (we always need to consider the first sample point).
+ var existing = BinarySearch(SamplePoints, time);
+ return newPoint.IsRedundant(existing);
- return base.CheckAlreadyExisting(time, newPoint);
+ case DifficultyControlPoint _:
+ return newPoint.IsRedundant(DifficultyPointAt(time));
+
+ default:
+ return base.CheckAlreadyExisting(time, newPoint);
+ }
}
protected override void GroupItemAdded(ControlPoint controlPoint)
{
- if (controlPoint is SampleControlPoint typed)
- samplePoints.Add(typed);
+ switch (controlPoint)
+ {
+ case SampleControlPoint typed:
+ samplePoints.Add(typed);
+ return;
- base.GroupItemAdded(controlPoint);
+ case DifficultyControlPoint typed:
+ difficultyPoints.Add(typed);
+ return;
+
+ default:
+ base.GroupItemAdded(controlPoint);
+ break;
+ }
}
protected override void GroupItemRemoved(ControlPoint controlPoint)
{
- if (controlPoint is SampleControlPoint typed)
- samplePoints.Remove(typed);
+ switch (controlPoint)
+ {
+ case SampleControlPoint typed:
+ samplePoints.Remove(typed);
+ break;
+
+ case DifficultyControlPoint typed:
+ difficultyPoints.Remove(typed);
+ break;
+ }
base.GroupItemRemoved(controlPoint);
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 18adecb7aa..d2c0f7de0f 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -189,11 +189,14 @@ namespace osu.Game.Beatmaps
///
public void CancelAsyncLoad()
{
- loadCancellation?.Cancel();
- loadCancellation = new CancellationTokenSource();
+ lock (beatmapFetchLock)
+ {
+ loadCancellation?.Cancel();
+ loadCancellation = new CancellationTokenSource();
- if (beatmapLoadTask?.IsCompleted != true)
- beatmapLoadTask = null;
+ if (beatmapLoadTask?.IsCompleted != true)
+ beatmapLoadTask = null;
+ }
}
private CancellationTokenSource createCancellationTokenSource(TimeSpan? timeout)
@@ -205,19 +208,27 @@ namespace osu.Game.Beatmaps
return new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10));
}
- private Task loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() =>
+ private readonly object beatmapFetchLock = new object();
+
+ private Task loadBeatmapAsync()
{
- // Todo: Handle cancellation during beatmap parsing
- var b = GetBeatmap() ?? new Beatmap();
+ lock (beatmapFetchLock)
+ {
+ return beatmapLoadTask ??= Task.Factory.StartNew(() =>
+ {
+ // Todo: Handle cancellation during beatmap parsing
+ var b = GetBeatmap() ?? new Beatmap();
- // The original beatmap version needs to be preserved as the database doesn't contain it
- BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion;
+ // The original beatmap version needs to be preserved as the database doesn't contain it
+ BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion;
- // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc)
- b.BeatmapInfo = BeatmapInfo;
+ // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc)
+ b.BeatmapInfo = BeatmapInfo;
- return b;
- }, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ return b;
+ }, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ }
+ }
public override string ToString() => BeatmapInfo.ToString();
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index ad3e890b3a..cf83345e2a 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -66,8 +66,12 @@ namespace osu.Game.Beatmaps
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID);
+
if (working != null)
+ {
+ Logger.Log($"Invalidating working beatmap cache for {info}");
workingCache.Remove(working);
+ }
}
}
@@ -86,6 +90,7 @@ namespace osu.Game.Beatmaps
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
+
if (working != null)
return working;
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index c235fc7728..84e33e3f36 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Database
///
/// The model type.
/// The associated file join type.
- public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager, IPostImports
+ public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager
where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete
where TFileModel : class, INamedFileInfo, new()
{
diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs
index 8e658cb0f5..479f33c3b4 100644
--- a/osu.Game/Database/IModelImporter.cs
+++ b/osu.Game/Database/IModelImporter.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Database
/// 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/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs
index c3810eb441..82d51e365e 100644
--- a/osu.Game/Database/RealmContextFactory.cs
+++ b/osu.Game/Database/RealmContextFactory.cs
@@ -5,7 +5,6 @@ using System;
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 @@ namespace osu.Game.Database
///
public bool Compact() => Realm.Compact(getConfiguration());
- protected override void Update()
+ ///
+ /// Perform a blocking refresh on the main realm context.
+ ///
+ public void Refresh()
{
- base.Update();
-
lock (contextLock)
{
if (context?.Refresh() == true)
@@ -92,7 +92,7 @@ namespace osu.Game.Database
public Realm CreateContext()
{
- if (IsDisposed)
+ if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
try
@@ -132,7 +132,7 @@ namespace osu.Game.Database
/// An which should be disposed to end the blocking section.
public IDisposable BlockAllOperations()
{
- if (IsDisposed)
+ if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
if (!ThreadSafety.IsUpdateThread)
@@ -176,21 +176,23 @@ namespace osu.Game.Database
});
}
- protected override void Dispose(bool isDisposing)
+ private bool isDisposed;
+
+ public void Dispose()
{
lock (contextLock)
{
context?.Dispose();
}
- if (!IsDisposed)
+ if (!isDisposed)
{
// intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal.
contextCreationLock.Wait();
contextCreationLock.Dispose();
- }
- base.Dispose(isDisposing);
+ isDisposed = true;
+ }
}
}
}
diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs
index aaad72f65c..017ea6ec32 100644
--- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs
@@ -1,11 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@@ -141,12 +144,12 @@ namespace osu.Game.Graphics.Containers
Child = box = new Box { RelativeSizeAxes = Axes.Both };
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
Colour = defaultColour = colours.Gray8;
hoverColour = colours.GrayF;
- highlightColour = colours.Green;
+ highlightColour = colourProvider?.Highlight1 ?? colours.Green;
}
public override void ResizeTo(float val, int duration = 0, Easing easing = Easing.None)
diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs
index 6807d007bb..8f0fed580f 100644
--- a/osu.Game/Graphics/UserInterface/Nub.cs
+++ b/osu.Game/Graphics/UserInterface/Nub.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using JetBrains.Annotations;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
@@ -12,63 +13,74 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
+using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface
{
- public class Nub : CircularContainer, IHasCurrentValue, IHasAccentColour
+ public class Nub : CompositeDrawable, IHasCurrentValue, IHasAccentColour
{
- public const float COLLAPSED_SIZE = 20;
- public const float EXPANDED_SIZE = 40;
+ public const float HEIGHT = 15;
+
+ public const float EXPANDED_SIZE = 50;
private const float border_width = 3;
- private const double animate_in_duration = 150;
+ private const double animate_in_duration = 200;
private const double animate_out_duration = 500;
+ private readonly Box fill;
+ private readonly Container main;
+
public Nub()
{
- Box fill;
+ Size = new Vector2(EXPANDED_SIZE, HEIGHT);
- Size = new Vector2(COLLAPSED_SIZE, 12);
-
- BorderColour = Color4.White;
- BorderThickness = border_width;
-
- Masking = true;
-
- Children = new[]
+ InternalChildren = new[]
{
- fill = new Box
+ main = new CircularContainer
{
+ BorderColour = Color4.White,
+ BorderThickness = border_width,
+ Masking = true,
RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Children = new Drawable[]
+ {
+ fill = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ },
+ }
},
};
-
- Current.ValueChanged += filled =>
- {
- fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint);
- this.TransformTo(nameof(BorderThickness), filled.NewValue ? 8.5f : border_width, 200, Easing.OutQuint);
- };
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour colours)
{
- AccentColour = colours.Pink;
- GlowingAccentColour = colours.PinkLighter;
- GlowColour = colours.PinkDarker;
+ AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
+ GlowingAccentColour = colourProvider?.Highlight1.Lighten(0.2f) ?? colours.PinkLighter;
+ GlowColour = colourProvider?.Highlight1 ?? colours.PinkLighter;
- EdgeEffect = new EdgeEffectParameters
+ main.EdgeEffect = new EdgeEffectParameters
{
Colour = GlowColour.Opacity(0),
Type = EdgeEffectType.Glow,
- Radius = 10,
- Roundness = 8,
+ Radius = 8,
+ Roundness = 5,
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Current.BindValueChanged(onCurrentValueChanged, true);
+ }
+
private bool glowing;
public bool Glowing
@@ -80,28 +92,17 @@ namespace osu.Game.Graphics.UserInterface
if (value)
{
- this.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint);
- FadeEdgeEffectTo(1, animate_in_duration, Easing.OutQuint);
+ main.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint);
+ main.FadeEdgeEffectTo(0.2f, animate_in_duration, Easing.OutQuint);
}
else
{
- FadeEdgeEffectTo(0, animate_out_duration);
- this.FadeColour(AccentColour, animate_out_duration);
+ main.FadeEdgeEffectTo(0, animate_out_duration, Easing.OutQuint);
+ main.FadeColour(AccentColour, animate_out_duration, Easing.OutQuint);
}
}
}
- public bool Expanded
- {
- set
- {
- if (value)
- this.ResizeTo(new Vector2(EXPANDED_SIZE, 12), animate_in_duration, Easing.OutQuint);
- else
- this.ResizeTo(new Vector2(COLLAPSED_SIZE, 12), animate_out_duration, Easing.OutQuint);
- }
- }
-
private readonly Bindable current = new Bindable();
public Bindable Current
@@ -126,7 +127,7 @@ namespace osu.Game.Graphics.UserInterface
{
accentColour = value;
if (!Glowing)
- Colour = value;
+ main.Colour = value;
}
}
@@ -139,7 +140,7 @@ namespace osu.Game.Graphics.UserInterface
{
glowingAccentColour = value;
if (Glowing)
- Colour = value;
+ main.Colour = value;
}
}
@@ -152,10 +153,22 @@ namespace osu.Game.Graphics.UserInterface
{
glowColour = value;
- var effect = EdgeEffect;
+ var effect = main.EdgeEffect;
effect.Colour = Glowing ? value : value.Opacity(0);
- EdgeEffect = effect;
+ main.EdgeEffect = effect;
}
}
+
+ private void onCurrentValueChanged(ValueChangedEvent filled)
+ {
+ fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint);
+
+ if (filled.NewValue)
+ main.ResizeWidthTo(1, animate_in_duration, Easing.OutElasticHalf);
+ else
+ main.ResizeWidthTo(0.9f, animate_out_duration, Easing.OutElastic);
+
+ main.TransformTo(nameof(BorderThickness), filled.NewValue ? 8.5f : border_width, 200, Easing.OutQuint);
+ }
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
index 5f2d884cd7..e8f80dec57 100644
--- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
@@ -9,16 +9,11 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
-using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public class OsuCheckbox : Checkbox
{
- public Color4 CheckedColor { get; set; } = Color4.Cyan;
- public Color4 UncheckedColor { get; set; } = Color4.White;
- public int FadeDuration { get; set; }
-
///
/// Whether to play sounds when the state changes as a result of user interaction.
///
@@ -104,14 +99,12 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
Nub.Glowing = true;
- Nub.Expanded = true;
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
Nub.Glowing = false;
- Nub.Expanded = false;
base.OnHoverLost(e);
}
diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
index fe88e6f78a..5831d9ab1f 100644
--- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
@@ -1,8 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System.Linq;
-using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -14,13 +15,15 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
+using osu.Game.Overlays;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public class OsuDropdown : Dropdown, IHasAccentColour
{
- private const float corner_radius = 4;
+ private const float corner_radius = 5;
private Color4 accentColour;
@@ -34,11 +37,11 @@ namespace osu.Game.Graphics.UserInterface
}
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
if (accentColour == default)
- accentColour = colours.PinkDarker;
+ accentColour = colourProvider?.Light4 ?? colours.PinkDarker;
updateAccentColour();
}
@@ -59,14 +62,13 @@ namespace osu.Game.Graphics.UserInterface
{
public override bool HandleNonPositionalInput => State == MenuState.Open;
- private Sample sampleOpen;
- private Sample sampleClose;
+ private Sample? sampleOpen;
+ private Sample? sampleClose;
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
public OsuDropdownMenu()
{
CornerRadius = corner_radius;
- BackgroundColour = Color4.Black.Opacity(0.5f);
MaskingContainer.CornerRadius = corner_radius;
Alpha = 0;
@@ -75,9 +77,11 @@ namespace osu.Game.Graphics.UserInterface
ItemsContainer.Padding = new MarginPadding(5);
}
- [BackgroundDependencyLoader]
- private void load(AudioManager audio)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, AudioManager audio)
{
+ BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
+
sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
sampleClose = audio.Samples.Get(@"UI/dropdown-close");
}
@@ -159,6 +163,8 @@ namespace osu.Game.Graphics.UserInterface
{
BackgroundColourHover = accentColour ?? nonAccentHoverColour;
BackgroundColourSelected = accentColour ?? nonAccentSelectedColour;
+ BackgroundColour = BackgroundColourHover.Opacity(0);
+
UpdateBackgroundColour();
UpdateForegroundColour();
}
@@ -178,8 +184,6 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- BackgroundColour = Color4.Transparent;
-
nonAccentHoverColour = colours.PinkDarker;
nonAccentSelectedColour = Color4.Black.Opacity(0.5f);
updateColours();
@@ -187,16 +191,29 @@ namespace osu.Game.Graphics.UserInterface
AddInternal(new HoverSounds());
}
+ protected override void UpdateBackgroundColour()
+ {
+ if (!IsPreSelected && !IsSelected)
+ {
+ Background.FadeOut(600, Easing.OutQuint);
+ return;
+ }
+
+ Background.FadeIn(100, Easing.OutQuint);
+ Background.FadeColour(IsPreSelected ? BackgroundColourHover : BackgroundColourSelected, 100, Easing.OutQuint);
+ }
+
protected override void UpdateForegroundColour()
{
base.UpdateForegroundColour();
- if (Foreground.Children.FirstOrDefault() is Content content) content.Chevron.Alpha = IsHovered ? 1 : 0;
+ if (Foreground.Children.FirstOrDefault() is Content content)
+ content.Hovering = IsHovered;
}
protected override Drawable CreateContent() => new Content();
- protected new class Content : FillFlowContainer, IHasText
+ protected new class Content : CompositeDrawable, IHasText
{
public LocalisableString Text
{
@@ -207,32 +224,64 @@ namespace osu.Game.Graphics.UserInterface
public readonly OsuSpriteText Label;
public readonly SpriteIcon Chevron;
+ private const float chevron_offset = -3;
+
public Content()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- Direction = FillDirection.Horizontal;
- Children = new Drawable[]
+ InternalChildren = new Drawable[]
{
Chevron = new SpriteIcon
{
- AlwaysPresent = true,
Icon = FontAwesome.Solid.ChevronRight,
- Colour = Color4.Black,
- Alpha = 0.5f,
Size = new Vector2(8),
+ Alpha = 0,
+ X = chevron_offset,
Margin = new MarginPadding { Left = 3, Right = 3 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
Label = new OsuSpriteText
{
+ X = 15,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
};
}
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider)
+ {
+ Chevron.Colour = colourProvider?.Background5 ?? Color4.Black;
+ }
+
+ private bool hovering;
+
+ public bool Hovering
+ {
+ get => hovering;
+ set
+ {
+ if (value == hovering)
+ return;
+
+ hovering = value;
+
+ if (hovering)
+ {
+ Chevron.FadeIn(400, Easing.OutQuint);
+ Chevron.MoveToX(0, 400, Easing.OutQuint);
+ }
+ else
+ {
+ Chevron.FadeOut(200);
+ Chevron.MoveToX(chevron_offset, 200, Easing.In);
+ }
+ }
+ }
}
}
@@ -267,7 +316,7 @@ namespace osu.Game.Graphics.UserInterface
public OsuDropdownHeader()
{
- Foreground.Padding = new MarginPadding(4);
+ Foreground.Padding = new MarginPadding(10);
AutoSizeAxes = Axes.None;
Margin = new MarginPadding { Bottom = 4 };
@@ -303,8 +352,7 @@ namespace osu.Game.Graphics.UserInterface
Icon = FontAwesome.Solid.ChevronDown,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
- Margin = new MarginPadding { Horizontal = 5 },
- Size = new Vector2(12),
+ Size = new Vector2(16),
},
}
}
@@ -313,11 +361,11 @@ namespace osu.Game.Graphics.UserInterface
AddInternal(new HoverClickSounds());
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
- BackgroundColour = Color4.Black.Opacity(0.5f);
- BackgroundColourHover = colours.PinkDarker;
+ BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
+ BackgroundColourHover = colourProvider?.Light4 ?? colours.PinkDarker;
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index f85f9327fa..6963f7335e 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -3,11 +3,13 @@
using System;
using System.Globalization;
+using JetBrains.Annotations;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
@@ -16,6 +18,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
+using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface
{
@@ -52,34 +55,63 @@ namespace osu.Game.Graphics.UserInterface
{
accentColour = value;
leftBox.Colour = value;
+ }
+ }
+
+ private Colour4 backgroundColour;
+
+ public Color4 BackgroundColour
+ {
+ get => backgroundColour;
+ set
+ {
+ backgroundColour = value;
rightBox.Colour = value;
}
}
public OsuSliderBar()
{
- Height = 12;
- RangePadding = 20;
+ Height = Nub.HEIGHT;
+ RangePadding = Nub.EXPANDED_SIZE / 2;
Children = new Drawable[]
{
- leftBox = new Box
+ new Container
{
- Height = 2,
- EdgeSmoothness = new Vector2(0, 0.5f),
- Position = new Vector2(2, 0),
- RelativeSizeAxes = Axes.None,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
- },
- rightBox = new Box
- {
- Height = 2,
- EdgeSmoothness = new Vector2(0, 0.5f),
- Position = new Vector2(-2, 0),
- RelativeSizeAxes = Axes.None,
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- Alpha = 0.5f,
+ Padding = new MarginPadding { Horizontal = 2 },
+ Child = new CircularContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Masking = true,
+ CornerRadius = 5f,
+ Children = new Drawable[]
+ {
+ leftBox = new Box
+ {
+ Height = 5,
+ EdgeSmoothness = new Vector2(0, 0.5f),
+ RelativeSizeAxes = Axes.None,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ rightBox = new Box
+ {
+ Height = 5,
+ EdgeSmoothness = new Vector2(0, 0.5f),
+ RelativeSizeAxes = Axes.None,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Alpha = 0.5f,
+ },
+ },
+ },
},
nubContainer = new Container
{
@@ -88,7 +120,7 @@ namespace osu.Game.Graphics.UserInterface
{
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
- Expanded = true,
+ Current = { Value = true }
},
},
new HoverClickSounds()
@@ -97,11 +129,12 @@ namespace osu.Game.Graphics.UserInterface
Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; };
}
- [BackgroundDependencyLoader]
- private void load(AudioManager audio, OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load(AudioManager audio, [CanBeNull] OverlayColourProvider colourProvider, OsuColour colours)
{
sample = audio.Samples.Get(@"UI/notch-tick");
- AccentColour = colours.Pink;
+ AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
+ BackgroundColour = colourProvider?.Background5 ?? colours.Pink.Opacity(0.5f);
}
protected override void Update()
@@ -119,26 +152,25 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
- Nub.Glowing = true;
+ updateGlow();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
- Nub.Glowing = false;
+ updateGlow();
base.OnHoverLost(e);
}
- protected override bool OnMouseDown(MouseDownEvent e)
+ protected override void OnDragEnd(DragEndEvent e)
{
- Nub.Current.Value = true;
- return base.OnMouseDown(e);
+ updateGlow();
+ base.OnDragEnd(e);
}
- protected override void OnMouseUp(MouseUpEvent e)
+ private void updateGlow()
{
- Nub.Current.Value = false;
- base.OnMouseUp(e);
+ Nub.Glowing = IsHovered || IsDragged;
}
protected override void OnUserChange(T value)
diff --git a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs
index 965734792c..c01ee1a059 100644
--- a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs
@@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
-using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
@@ -15,30 +12,13 @@ namespace osu.Game.Graphics.UserInterface
{
protected override DropdownHeader CreateHeader() => new SlimDropdownHeader();
- protected override DropdownMenu CreateMenu() => new SlimMenu();
-
private class SlimDropdownHeader : OsuDropdownHeader
{
public SlimDropdownHeader()
{
Height = 25;
- Icon.Size = new Vector2(16);
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 };
}
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
- BackgroundColour = Color4.Black.Opacity(0.25f);
- }
- }
-
- private class SlimMenu : OsuDropdownMenu
- {
- public SlimMenu()
- {
- BackgroundColour = Color4.Black.Opacity(0.7f);
- }
}
}
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
index 5a697623c9..d5f76733cf 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
@@ -1,12 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
+using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
@@ -44,6 +47,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
///
protected readonly T Component;
+ private readonly Box background;
private readonly GridContainer grid;
private readonly OsuTextFlowContainer labelText;
private readonly OsuTextFlowContainer descriptionText;
@@ -62,10 +66,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
InternalChildren = new Drawable[]
{
- new Box
+ background = new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex("1c2125"),
},
new FillFlowContainer
{
@@ -146,9 +149,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
}
- [BackgroundDependencyLoader]
- private void load(OsuColour osuColour)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour osuColour)
{
+ background.Colour = colourProvider?.Background4 ?? Color4Extensions.FromHex(@"1c2125");
descriptionText.Colour = osuColour.Yellow;
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
index 27e28f1e03..23ebc6e98d 100644
--- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
@@ -2,10 +2,12 @@
// 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
{
@@ -23,10 +25,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours)
{
- BackgroundColour = colours.Blue3;
+ BackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3;
}
protected override void LoadComplete()
diff --git a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs
index a7fd25b554..deb2e6baf6 100644
--- a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -10,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
+using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@@ -66,11 +69,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
};
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
- enabledColour = colours.BlueDark;
- disabledColour = colours.Gray3;
+ enabledColour = colourProvider?.Highlight1 ?? colours.BlueDark;
+ disabledColour = colourProvider?.Background3 ?? colours.Gray3;
switchContainer.Colour = enabledColour;
fill.Colour = disabledColour;
diff --git a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
new file mode 100644
index 0000000000..f5709b5158
--- /dev/null
+++ b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.IO;
+
+namespace osu.Game.IO.FileAbstraction
+{
+ public class StreamFileAbstraction : TagLib.File.IFileAbstraction
+ {
+ public StreamFileAbstraction(string filename, Stream fileStream)
+ {
+ ReadStream = fileStream;
+ Name = filename;
+ }
+
+ public string Name { get; }
+
+ public Stream ReadStream { get; }
+ public Stream WriteStream => ReadStream;
+
+ public void CloseStream(Stream stream)
+ {
+ if (stream == null)
+ throw new ArgumentNullException(nameof(stream));
+
+ stream.Close();
+ }
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 75bbaec0ef..28505f6b0e 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -374,7 +374,7 @@ namespace osu.Game.Online.Multiplayer
UserJoined?.Invoke(user);
RoomUpdated?.Invoke();
- }, false);
+ });
}
Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) =>
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 020cdebab6..820597488b 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -554,6 +554,7 @@ namespace osu.Game
{
beatmap.OldValue?.CancelAsyncLoad();
beatmap.NewValue?.BeginAsyncLoad();
+ Logger.Log($"Game-wide working beatmap updated to {beatmap.NewValue}");
}
private void modsChanged(ValueChangedEvent> mods)
@@ -642,7 +643,7 @@ namespace osu.Game
SkinManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PostNotification = n => Notifications.Post(n);
- BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value);
+ BeatmapManager.PostImport = items => PresentBeatmap(items.First().Value);
ScoreManager.PostNotification = n => Notifications.Post(n);
ScoreManager.PostImport = items => PresentScore(items.First().Value);
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 09eb482d16..f6ec22a536 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -187,8 +187,6 @@ namespace osu.Game
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client"));
- AddInternal(realmFactory);
-
dependencies.CacheAs(Storage);
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures")));
@@ -529,6 +527,7 @@ namespace osu.Game
LocalConfig?.Dispose();
contextFactory?.FlushConnections();
+ realmFactory?.Dispose();
}
}
}
diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs
index 3105ecd742..f8cd31f193 100644
--- a/osu.Game/Overlays/Notifications/ProgressNotification.cs
+++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs
@@ -31,10 +31,12 @@ namespace osu.Game.Overlays.Notifications
set
{
progress = value;
- Scheduler.AddOnce(() => progressBar.Progress = progress);
+ Scheduler.AddOnce(updateProgress, progress);
}
}
+ private void updateProgress(float progress) => progressBar.Progress = progress;
+
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs
index 1175ddaab8..a281d03ee7 100644
--- a/osu.Game/Overlays/Settings/SettingsDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs
@@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
+using osuTK;
namespace osu.Game.Overlays.Settings
{
@@ -27,6 +28,11 @@ namespace osu.Game.Overlays.Settings
public override IEnumerable FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString()));
+ public SettingsDropdown()
+ {
+ FlowContent.Spacing = new Vector2(0, 10);
+ }
+
protected sealed override Drawable CreateControl() => CreateDropdown();
protected virtual OsuDropdown CreateDropdown() => new DropdownControl();
@@ -35,7 +41,6 @@ namespace osu.Game.Overlays.Settings
{
public DropdownControl()
{
- Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X;
}
diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
index 9987a0c607..199ba14b48 100644
--- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
@@ -16,7 +16,6 @@ namespace osu.Game.Overlays.Settings
{
public DropdownControl()
{
- Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X;
}
diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs
index 9fc3379b94..bb9c0dd4d7 100644
--- a/osu.Game/Overlays/Settings/SettingsSlider.cs
+++ b/osu.Game/Overlays/Settings/SettingsSlider.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings
{
protected override Drawable CreateControl() => new TSlider
{
- Margin = new MarginPadding { Top = 5, Bottom = 5 },
+ Margin = new MarginPadding { Vertical = 10 },
RelativeSizeAxes = Axes.X
};
diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
index a0ec8e3e0e..eec71a3623 100644
--- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
+++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
@@ -47,9 +47,34 @@ namespace osu.Game.Rulesets.Configuration
}
}
+ private readonly HashSet pendingWrites = new HashSet();
+
protected override bool PerformSave()
{
- // do nothing, realm saves immediately
+ TLookup[] changed;
+
+ lock (pendingWrites)
+ {
+ changed = pendingWrites.ToArray();
+ pendingWrites.Clear();
+ }
+
+ if (realmFactory == null)
+ return true;
+
+ using (var context = realmFactory.CreateContext())
+ {
+ context.Write(realm =>
+ {
+ foreach (var c in changed)
+ {
+ var setting = realm.All().First(s => s.RulesetID == rulesetId && s.Variant == variant && s.Key == c.ToString());
+
+ setting.Value = ConfigStore[c].ToString();
+ }
+ });
+ }
+
return true;
}
@@ -80,7 +105,8 @@ namespace osu.Game.Rulesets.Configuration
bindable.ValueChanged += b =>
{
- realmFactory?.Context.Write(() => setting.Value = b.NewValue.ToString());
+ lock (pendingWrites)
+ pendingWrites.Add(lookup);
};
}
}
diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
index 81f4808789..6ed91e983a 100644
--- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
+++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
@@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Edit
new CheckAudioQuality(),
new CheckMutedObjects(),
new CheckFewHitsounds(),
+ new CheckTooShortAudioFiles(),
+ new CheckAudioInVideo(),
+
+ // Files
+ new CheckZeroByteFiles(),
// Compose
new CheckUnsnappedObjects(),
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs
new file mode 100644
index 0000000000..ac2542beb0
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs
@@ -0,0 +1,112 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using osu.Game.IO.FileAbstraction;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Storyboards;
+using TagLib;
+using File = TagLib.File;
+
+namespace osu.Game.Rulesets.Edit.Checks
+{
+ public class CheckAudioInVideo : ICheck
+ {
+ public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Audio track in video files");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateHasAudioTrack(this),
+ new IssueTemplateMissingFile(this),
+ new IssueTemplateFileError(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
+ var videoPaths = new List();
+
+ foreach (var layer in context.WorkingBeatmap.Storyboard.Layers)
+ {
+ foreach (var element in layer.Elements)
+ {
+ if (!(element is StoryboardVideo video))
+ continue;
+
+ // Ensures we don't check the same video file multiple times in case of multiple elements using it.
+ if (!videoPaths.Contains(video.Path))
+ videoPaths.Add(video.Path);
+ }
+ }
+
+ foreach (var filename in videoPaths)
+ {
+ string storagePath = beatmapSet.GetPathForFile(filename);
+
+ if (storagePath == null)
+ {
+ // There's an element in the storyboard that requires this resource, so it being missing is worth warning about.
+ yield return new IssueTemplateMissingFile(this).Create(filename);
+
+ continue;
+ }
+
+ Issue issue;
+
+ try
+ {
+ // We use TagLib here for platform invariance; BASS cannot detect audio presence on Linux.
+ using (Stream data = context.WorkingBeatmap.GetStream(storagePath))
+ using (File tagFile = File.Create(new StreamFileAbstraction(filename, data)))
+ {
+ if (tagFile.Properties.AudioChannels == 0)
+ continue;
+ }
+
+ issue = new IssueTemplateHasAudioTrack(this).Create(filename);
+ }
+ catch (CorruptFileException)
+ {
+ issue = new IssueTemplateFileError(this).Create(filename, "Corrupt file");
+ }
+ catch (UnsupportedFormatException)
+ {
+ issue = new IssueTemplateFileError(this).Create(filename, "Unsupported format");
+ }
+
+ yield return issue;
+ }
+ }
+
+ public class IssueTemplateHasAudioTrack : IssueTemplate
+ {
+ public IssueTemplateHasAudioTrack(ICheck check)
+ : base(check, IssueType.Problem, "\"{0}\" has an audio track.")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename);
+ }
+
+ public class IssueTemplateFileError : IssueTemplate
+ {
+ public IssueTemplateFileError(ICheck check)
+ : base(check, IssueType.Error, "Could not check whether \"{0}\" has an audio track ({1}).")
+ {
+ }
+
+ public Issue Create(string filename, string errorReason) => new Issue(this, filename, errorReason);
+ }
+
+ public class IssueTemplateMissingFile : IssueTemplate
+ {
+ public IssueTemplateMissingFile(ICheck check)
+ : base(check, IssueType.Warning, "Could not check whether \"{0}\" has an audio track, because it is missing.")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs
new file mode 100644
index 0000000000..57f7c60916
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using ManagedBass;
+using osu.Framework.Audio.Callbacks;
+using osu.Game.Rulesets.Edit.Checks.Components;
+
+namespace osu.Game.Rulesets.Edit.Checks
+{
+ public class CheckTooShortAudioFiles : ICheck
+ {
+ private const int ms_threshold = 25;
+ private const int min_bytes_threshold = 100;
+
+ private readonly string[] audioExtensions = { "mp3", "ogg", "wav" };
+
+ public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateTooShort(this),
+ new IssueTemplateBadFormat(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
+
+ foreach (var file in beatmapSet.Files)
+ {
+ using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.StoragePath))
+ {
+ if (data == null)
+ continue;
+
+ var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data));
+ int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle);
+
+ if (decodeStream == 0)
+ {
+ // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it.
+ // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check.
+ if (hasAudioExtension(file.Filename) && probablyHasAudioData(data))
+ yield return new IssueTemplateBadFormat(this).Create(file.Filename);
+
+ continue;
+ }
+
+ long length = Bass.ChannelGetLength(decodeStream);
+ double ms = Bass.ChannelBytes2Seconds(decodeStream, length) * 1000;
+
+ // Extremely short audio files do not play on some soundcards, resulting in nothing being heard in-game for some users.
+ if (ms > 0 && ms < ms_threshold)
+ yield return new IssueTemplateTooShort(this).Create(file.Filename, ms);
+ }
+ }
+ }
+
+ private bool hasAudioExtension(string filename) => audioExtensions.Any(filename.ToLower().EndsWith);
+ private bool probablyHasAudioData(Stream data) => data.Length > min_bytes_threshold;
+
+ public class IssueTemplateTooShort : IssueTemplate
+ {
+ public IssueTemplateTooShort(ICheck check)
+ : base(check, IssueType.Problem, "\"{0}\" is too short ({1:0} ms), should be at least {2:0} ms.")
+ {
+ }
+
+ public Issue Create(string filename, double ms) => new Issue(this, filename, ms, ms_threshold);
+ }
+
+ public class IssueTemplateBadFormat : IssueTemplate
+ {
+ public IssueTemplateBadFormat(ICheck check)
+ : base(check, IssueType.Error, "Could not check whether \"{0}\" is too short (code \"{1}\").")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename, Bass.LastError);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs
new file mode 100644
index 0000000000..3a994fabfa
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using osu.Game.Rulesets.Edit.Checks.Components;
+
+namespace osu.Game.Rulesets.Edit.Checks
+{
+ public class CheckZeroByteFiles : ICheck
+ {
+ public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Files, "Zero-byte files");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateZeroBytes(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
+
+ foreach (var file in beatmapSet.Files)
+ {
+ using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.StoragePath))
+ {
+ if (data?.Length == 0)
+ yield return new IssueTemplateZeroBytes(this).Create(file.Filename);
+ }
+ }
+ }
+
+ public class IssueTemplateZeroBytes : IssueTemplate
+ {
+ public IssueTemplateZeroBytes(ICheck check)
+ : base(check, IssueType.Problem, "\"{0}\" is a 0-byte file.")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index b41e0442bc..91cc80e930 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -13,7 +13,6 @@ using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@@ -389,41 +388,42 @@ namespace osu.Game.Rulesets.Edit
return new SnapResult(screenSpacePosition, targetTime, playfield);
}
- public override float GetBeatSnapDistanceAt(double referenceTime)
+ public override float GetBeatSnapDistanceAt(HitObject referenceObject)
{
- DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime);
- return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / BeatSnapProvider.BeatDivisor);
+ return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * referenceObject.DifficultyControlPoint.SliderVelocity / BeatSnapProvider.BeatDivisor);
}
- public override float DurationToDistance(double referenceTime, double duration)
+ public override float DurationToDistance(HitObject referenceObject, double duration)
{
- double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
- return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime));
+ double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
+ return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject));
}
- public override double DistanceToDuration(double referenceTime, float distance)
+ public override double DistanceToDuration(HitObject referenceObject, float distance)
{
- double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
- return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength;
+ double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
+ return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength;
}
- public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
- => BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime;
+ public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
+ => BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime;
- public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
+ public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
{
- double actualDuration = referenceTime + DistanceToDuration(referenceTime, distance);
+ double startTime = referenceObject.StartTime;
- double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, referenceTime);
+ double actualDuration = startTime + DistanceToDuration(referenceObject, distance);
- double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
+ double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, startTime);
+
+ double beatLength = BeatSnapProvider.GetBeatLengthAtTime(startTime);
// we don't want to exceed the actual duration and snap to a point in the future.
// as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it.
if (snappedEndTime > actualDuration + 1)
snappedEndTime -= beatLength;
- return DurationToDistance(referenceTime, snappedEndTime - referenceTime);
+ return DurationToDistance(referenceObject, snappedEndTime - startTime);
}
#endregion
@@ -466,15 +466,15 @@ namespace osu.Game.Rulesets.Edit
public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null);
- public abstract float GetBeatSnapDistanceAt(double referenceTime);
+ public abstract float GetBeatSnapDistanceAt(HitObject referenceObject);
- public abstract float DurationToDistance(double referenceTime, double duration);
+ public abstract float DurationToDistance(HitObject referenceObject, double duration);
- public abstract double DistanceToDuration(double referenceTime, float distance);
+ public abstract double DistanceToDuration(HitObject referenceObject, float distance);
- public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance);
+ public abstract double GetSnappedDurationFromDistance(HitObject referenceObject, float distance);
- public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance);
+ public abstract float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance);
#endregion
}
diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
index 4664f3808c..743a2f41fc 100644
--- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
+++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Edit
@@ -27,41 +28,41 @@ namespace osu.Game.Rulesets.Edit
///
/// Retrieves the distance between two points within a timing point that are one beat length apart.
///
- /// The time of the timing point.
+ /// An object to be used as a reference point for this operation.
/// The distance between two points residing in the timing point that are one beat length apart.
- float GetBeatSnapDistanceAt(double referenceTime);
+ float GetBeatSnapDistanceAt(HitObject referenceObject);
///
/// Converts a duration to a distance.
///
- /// The time of the timing point which resides in.
+ /// An object to be used as a reference point for this operation.
/// The duration to convert.
/// A value that represents as a distance in the timing point.
- float DurationToDistance(double referenceTime, double duration);
+ float DurationToDistance(HitObject referenceObject, double duration);
///
/// Converts a distance to a duration.
///
- /// The time of the timing point which resides in.
+ /// An object to be used as a reference point for this operation.
/// The distance to convert.
/// A value that represents as a duration in the timing point.
- double DistanceToDuration(double referenceTime, float distance);
+ double DistanceToDuration(HitObject referenceObject, float distance);
///
/// Converts a distance to a snapped duration.
///
- /// The time of the timing point which resides in.
+ /// An object to be used as a reference point for this operation.
/// The distance to convert.
/// A value that represents as a duration snapped to the closest beat of the timing point.
- double GetSnappedDurationFromDistance(double referenceTime, float distance);
+ double GetSnappedDurationFromDistance(HitObject referenceObject, float distance);
///
/// Converts an unsnapped distance to a snapped distance.
/// The returned distance will always be floored (as to never exceed the provided .
///
- /// The time of the timing point which resides in.
+ /// An object to be used as a reference point for this operation.
/// The distance to convert.
/// A value that represents snapped to the closest beat of the timing point.
- float GetSnappedDistanceFromDistance(double referenceTime, float distance);
+ float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance);
}
}
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index 0b159819d4..035ebe10cb 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -67,7 +67,8 @@ namespace osu.Game.Rulesets.Objects
}
}
- public SampleControlPoint SampleControlPoint;
+ public SampleControlPoint SampleControlPoint = SampleControlPoint.DEFAULT;
+ public DifficultyControlPoint DifficultyControlPoint = DifficultyControlPoint.DEFAULT;
///
/// Whether this is in Kiai time.
@@ -94,6 +95,12 @@ namespace osu.Game.Rulesets.Objects
foreach (var nested in nestedHitObjects)
nested.StartTime += offset;
+
+ if (DifficultyControlPoint != DifficultyControlPoint.DEFAULT)
+ DifficultyControlPoint.Time = time.NewValue;
+
+ if (SampleControlPoint != SampleControlPoint.DEFAULT)
+ SampleControlPoint.Time = this.GetEndTime() + control_point_leniency;
};
}
@@ -105,16 +112,21 @@ namespace osu.Game.Rulesets.Objects
/// The cancellation token.
public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default)
{
+ var legacyInfo = controlPointInfo as LegacyControlPointInfo;
+
+ if (legacyInfo != null)
+ {
+ DifficultyControlPoint = (DifficultyControlPoint)legacyInfo.DifficultyPointAt(StartTime).DeepClone();
+ DifficultyControlPoint.Time = StartTime;
+ }
+
ApplyDefaultsToSelf(controlPointInfo, difficulty);
- if (controlPointInfo is LegacyControlPointInfo legacyInfo)
+ // This is done here after ApplyDefaultsToSelf as we may require custom defaults to be applied to have an accurate end time.
+ if (legacyInfo != null)
{
- // This is done here since ApplyDefaultsToSelf may be used to determine the end time
- SampleControlPoint = legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency);
- }
- else
- {
- SampleControlPoint ??= SampleControlPoint.DEFAULT;
+ SampleControlPoint = (SampleControlPoint)legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency).DeepClone();
+ SampleControlPoint.Time = this.GetEndTime() + control_point_leniency;
}
nestedHitObjects.Clear();
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
index e1de82ade7..ad191f7ff5 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
@@ -43,9 +43,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
}
diff --git a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
index dcd2cc8b55..23325bcd13 100644
--- a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
+++ b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
@@ -7,7 +7,7 @@ using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Timing
{
///
- /// A control point which adds an aggregated multiplier based on the provided 's BeatLength and 's SpeedMultiplier.
+ /// A control point which adds an aggregated multiplier based on the provided 's BeatLength and 's SpeedMultiplier.
///
public class MultiplierControlPoint : IComparable
{
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Timing
///
/// The aggregate multiplier which this provides.
///
- public double Multiplier => Velocity * DifficultyPoint.SpeedMultiplier * BaseBeatLength / TimingPoint.BeatLength;
+ public double Multiplier => Velocity * EffectPoint.ScrollSpeed * BaseBeatLength / TimingPoint.BeatLength;
///
/// The base beat length to scale the provided multiplier relative to.
@@ -38,9 +38,9 @@ namespace osu.Game.Rulesets.Timing
public TimingControlPoint TimingPoint = new TimingControlPoint();
///
- /// The that provides additional difficulty information for this .
+ /// The that provides additional difficulty information for this .
///
- public DifficultyControlPoint DifficultyPoint = new DifficultyControlPoint();
+ public EffectControlPoint EffectPoint = new EffectControlPoint();
///
/// Creates a . This is required for JSON serialization
diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
index 041c5ebef5..2a9d3d1cf0 100644
--- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
@@ -140,25 +140,32 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Merge sequences of timing and difficulty control points to create the aggregate "multiplier" control point
var lastTimingPoint = new TimingControlPoint();
- var lastDifficultyPoint = new DifficultyControlPoint();
+ var lastEffectPoint = new EffectControlPoint();
var allPoints = new SortedList(Comparer.Default);
+
allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints);
- allPoints.AddRange(Beatmap.ControlPointInfo.DifficultyPoints);
+ allPoints.AddRange(Beatmap.ControlPointInfo.EffectPoints);
// Generate the timing points, making non-timing changes use the previous timing change and vice-versa
var timingChanges = allPoints.Select(c =>
{
- if (c is TimingControlPoint timingPoint)
- lastTimingPoint = timingPoint;
- else if (c is DifficultyControlPoint difficultyPoint)
- lastDifficultyPoint = difficultyPoint;
+ switch (c)
+ {
+ case TimingControlPoint timingPoint:
+ lastTimingPoint = timingPoint;
+ break;
+
+ case EffectControlPoint difficultyPoint:
+ lastEffectPoint = difficultyPoint;
+ break;
+ }
return new MultiplierControlPoint(c.Time)
{
Velocity = Beatmap.Difficulty.SliderMultiplier,
BaseBeatLength = baseBeatLength,
TimingPoint = lastTimingPoint,
- DifficultyPoint = lastDifficultyPoint
+ EffectPoint = lastEffectPoint
};
});
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index cf22a8fda4..8494cdcd22 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -25,7 +25,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring
{
- public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles, IPostImports
+ public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles
{
private readonly Scheduler scheduler;
private readonly Func difficulties;
diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
index 730f482f83..6b32ff96c4 100644
--- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
@@ -5,14 +5,15 @@ using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
+using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public abstract class CircularDistanceSnapGrid : DistanceSnapGrid
{
- protected CircularDistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null)
- : base(startPosition, startTime, endTime)
+ protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
+ : base(referenceObject, startPosition, startTime, endTime)
{
}
@@ -79,7 +80,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 normalisedDirection = direction * new Vector2(1f / distance);
Vector2 snappedPosition = StartPosition + normalisedDirection * radialCount * radius;
- return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(StartTime, (snappedPosition - StartPosition).Length));
+ return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(ReferenceObject, (snappedPosition - StartPosition).Length));
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 59f88ac641..9d43e3258a 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
@@ -54,15 +55,20 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
private readonly double? endTime;
+ protected readonly HitObject ReferenceObject;
+
///
/// Creates a new .
///
+ /// A reference object to gather relevant difficulty values from.
/// The position at which the grid should start. The first tick is located one distance spacing length away from this point.
/// The snapping time at .
/// The time at which the snapping grid should end. If null, the grid will continue until the bounds of the screen are exceeded.
- protected DistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null)
+ protected DistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
{
+ ReferenceObject = referenceObject;
this.endTime = endTime;
+
StartPosition = startPosition;
StartTime = startTime;
@@ -80,7 +86,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updateSpacing()
{
- DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime);
+ DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject);
if (endTime == null)
MaxIntervals = int.MaxValue;
@@ -88,7 +94,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
// +1 is added since a snapped hitobject may have its start time slightly less than the snapped time due to floating point errors
double maxDuration = endTime.Value - StartTime + 1;
- MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(StartTime, DistanceSpacing));
+ MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(ReferenceObject, DistanceSpacing));
}
gridCache.Invalidate();
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
index 3248936765..21457ea273 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
@@ -1,27 +1,106 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit.Timing;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
- public class DifficultyPointPiece : TopPointPiece
+ public class DifficultyPointPiece : HitObjectPointPiece, IHasPopover
{
+ private readonly HitObject hitObject;
+
private readonly BindableNumber speedMultiplier;
- public DifficultyPointPiece(DifficultyControlPoint point)
- : base(point)
+ public DifficultyPointPiece(HitObject hitObject)
+ : base(hitObject.DifficultyControlPoint)
{
- speedMultiplier = point.SpeedMultiplierBindable.GetBoundCopy();
+ this.hitObject = hitObject;
- Y = Height;
+ speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy();
}
protected override void LoadComplete()
{
base.LoadComplete();
+
speedMultiplier.BindValueChanged(multiplier => Label.Text = $"{multiplier.NewValue:n2}x", true);
}
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ this.ShowPopover();
+ return true;
+ }
+
+ public Popover GetPopover() => new DifficultyEditPopover(hitObject);
+
+ public class DifficultyEditPopover : OsuPopover
+ {
+ private readonly HitObject hitObject;
+ private readonly DifficultyControlPoint point;
+
+ private SliderWithTextBoxInput sliderVelocitySlider;
+
+ [Resolved(canBeNull: true)]
+ private EditorBeatmap beatmap { get; set; }
+
+ public DifficultyEditPopover(HitObject hitObject)
+ {
+ this.hitObject = hitObject;
+ point = hitObject.DifficultyControlPoint;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ Width = 200,
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ sliderVelocitySlider = new SliderWithTextBoxInput("Velocity")
+ {
+ Current = new DifficultyControlPoint().SliderVelocityBindable,
+ KeyboardStep = 0.1f
+ },
+ new OsuTextFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Text = "Hold shift while dragging the end of an object to adjust velocity while snapping."
+ }
+ }
+ }
+ };
+
+ var selectedPointBindable = point.SliderVelocityBindable;
+
+ // there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint).
+ // generally that level of precision could only be set by externally editing the .osu file, so at the point
+ // a user is looking to update this within the editor it should be safe to obliterate this additional precision.
+ double expectedPrecision = new DifficultyControlPoint().SliderVelocityBindable.Precision;
+ if (selectedPointBindable.Precision < expectedPrecision)
+ selectedPointBindable.Precision = expectedPrecision;
+
+ sliderVelocitySlider.Current = selectedPointBindable;
+ sliderVelocitySlider.Current.BindValueChanged(_ => beatmap?.Update(hitObject));
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs
new file mode 100644
index 0000000000..6b62459c97
--- /dev/null
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit.Compose.Components.Timeline
+{
+ public class HitObjectPointPiece : CircularContainer
+ {
+ private readonly ControlPoint point;
+
+ protected OsuSpriteText Label { get; private set; }
+
+ protected HitObjectPointPiece(ControlPoint point)
+ {
+ this.point = point;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ AutoSizeAxes = Axes.Both;
+
+ Color4 colour = point.GetRepresentingColour(colours);
+
+ InternalChildren = new Drawable[]
+ {
+ new Container
+ {
+ AutoSizeAxes = Axes.X,
+ Height = 16,
+ Masking = true,
+ CornerRadius = 8,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colour,
+ RelativeSizeAxes = Axes.Both,
+ },
+ Label = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Padding = new MarginPadding(5),
+ Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
+ Colour = colours.B5,
+ }
+ }
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
index 9461f5e885..6a26f69e41 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
@@ -3,88 +3,102 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osuTK.Graphics;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit.Timing;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
- public class SamplePointPiece : CompositeDrawable
+ public class SamplePointPiece : HitObjectPointPiece, IHasPopover
{
- private readonly SampleControlPoint samplePoint;
+ private readonly HitObject hitObject;
private readonly Bindable bank;
private readonly BindableNumber volume;
- private OsuSpriteText text;
- private Container volumeBox;
-
- private const int max_volume_height = 22;
-
- public SamplePointPiece(SampleControlPoint samplePoint)
+ public SamplePointPiece(HitObject hitObject)
+ : base(hitObject.SampleControlPoint)
{
- this.samplePoint = samplePoint;
- volume = samplePoint.SampleVolumeBindable.GetBoundCopy();
- bank = samplePoint.SampleBankBindable.GetBoundCopy();
+ this.hitObject = hitObject;
+ volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy();
+ bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- Margin = new MarginPadding { Vertical = 5 };
+ volume.BindValueChanged(volume => updateText());
+ bank.BindValueChanged(bank => updateText(), true);
+ }
- Origin = Anchor.BottomCentre;
- Anchor = Anchor.BottomCentre;
+ protected override bool OnClick(ClickEvent e)
+ {
+ this.ShowPopover();
+ return true;
+ }
- AutoSizeAxes = Axes.X;
- RelativeSizeAxes = Axes.Y;
+ private void updateText()
+ {
+ Label.Text = $"{bank.Value} {volume.Value}";
+ }
- Color4 colour = samplePoint.GetRepresentingColour(colours);
+ public Popover GetPopover() => new SampleEditPopover(hitObject);
- InternalChildren = new Drawable[]
+ public class SampleEditPopover : OsuPopover
+ {
+ private readonly HitObject hitObject;
+ private readonly SampleControlPoint point;
+
+ private LabelledTextBox bank;
+ private SliderWithTextBoxInput volume;
+
+ [Resolved(canBeNull: true)]
+ private EditorBeatmap beatmap { get; set; }
+
+ public SampleEditPopover(HitObject hitObject)
{
- volumeBox = new Circle
+ this.hitObject = hitObject;
+ point = hitObject.SampleControlPoint;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
{
- CornerRadius = 5,
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Y = -20,
- Width = 10,
- Colour = colour,
- },
- new Container
- {
- AutoSizeAxes = Axes.X,
- Height = 16,
- Masking = true,
- CornerRadius = 8,
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Children = new Drawable[]
+ new FillFlowContainer
{
- new Box
+ Width = 200,
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
{
- Colour = colour,
- RelativeSizeAxes = Axes.Both,
- },
- text = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Padding = new MarginPadding(5),
- Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
- Colour = colours.B5,
+ bank = new LabelledTextBox
+ {
+ Label = "Bank Name",
+ },
+ volume = new SliderWithTextBoxInput("Volume")
+ {
+ Current = new SampleControlPoint().SampleVolumeBindable,
+ }
}
}
- },
- };
+ };
- volume.BindValueChanged(volume => volumeBox.Height = max_volume_height * volume.NewValue / 100f, true);
- bank.BindValueChanged(bank => text.Text = bank.NewValue, true);
+ bank.Current = point.SampleBankBindable;
+ bank.Current.BindValueChanged(_ => beatmap.Update(hitObject));
+
+ volume.Current = point.SampleVolumeBindable;
+ volume.Current.BindValueChanged(_ => beatmap.Update(hitObject));
+ }
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 621a24c67d..b8fa05e7eb 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -15,6 +15,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
@@ -58,7 +59,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Track track;
private const float timeline_height = 72;
- private const float timeline_expanded_height = 156;
+ private const float timeline_expanded_height = 94;
public Timeline(Drawable userContent)
{
@@ -158,7 +159,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (visible.NewValue)
{
this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint);
- mainContent.MoveToY(36, 200, Easing.OutQuint);
+ mainContent.MoveToY(20, 200, Easing.OutQuint);
// delay the fade in else masking looks weird.
controlPoints.Delay(180).FadeIn(400, Easing.OutQuint);
@@ -298,14 +299,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private double getTimeFromPosition(Vector2 localPosition) =>
(localPosition.X / Content.DrawWidth) * track.Length;
- public float GetBeatSnapDistanceAt(double referenceTime) => throw new NotImplementedException();
+ public float GetBeatSnapDistanceAt(HitObject referenceObject) => throw new NotImplementedException();
- public float DurationToDistance(double referenceTime, double duration) => throw new NotImplementedException();
+ public float DurationToDistance(HitObject referenceObject, double duration) => throw new NotImplementedException();
- public double DistanceToDuration(double referenceTime, float distance) => throw new NotImplementedException();
+ public double DistanceToDuration(HitObject referenceObject, float distance) => throw new NotImplementedException();
- public double GetSnappedDurationFromDistance(double referenceTime, float distance) => throw new NotImplementedException();
+ public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => throw new NotImplementedException();
- public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => throw new NotImplementedException();
+ public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => throw new NotImplementedException();
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
index c4beb40f92..2b2e66fb18 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
@@ -45,17 +45,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
switch (point)
{
- case DifficultyControlPoint difficultyPoint:
- AddInternal(new DifficultyPointPiece(difficultyPoint) { Depth = -2 });
- break;
-
case TimingControlPoint timingPoint:
AddInternal(new TimingPointPiece(timingPoint));
break;
-
- case SampleControlPoint samplePoint:
- AddInternal(new SamplePointPiece(samplePoint) { Depth = -1 });
- break;
}
}
}, true);
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index 911c9fea51..e2458d45c9 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -13,7 +13,9 @@ using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Threading;
using osu.Framework.Utils;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Edit;
@@ -179,6 +181,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
colouredComponents.Colour = OsuColour.ForegroundTextColourFor(col);
}
+ private SamplePointPiece sampleOverrideDisplay;
+ private DifficultyPointPiece difficultyOverrideDisplay;
+
+ [Resolved]
+ private EditorBeatmap beatmap { get; set; }
+
+ private DifficultyControlPoint difficultyControlPoint;
+ private SampleControlPoint sampleControlPoint;
+
protected override void Update()
{
base.Update();
@@ -194,6 +205,36 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (Item is IHasRepeats repeats)
updateRepeats(repeats);
}
+
+ if (difficultyControlPoint != Item.DifficultyControlPoint)
+ {
+ difficultyControlPoint = Item.DifficultyControlPoint;
+ difficultyOverrideDisplay?.Expire();
+
+ if (Item.DifficultyControlPoint != null && Item is IHasDistance)
+ {
+ AddInternal(difficultyOverrideDisplay = new DifficultyPointPiece(Item)
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.BottomCentre
+ });
+ }
+ }
+
+ if (sampleControlPoint != Item.SampleControlPoint)
+ {
+ sampleControlPoint = Item.SampleControlPoint;
+ sampleOverrideDisplay?.Expire();
+
+ if (Item.SampleControlPoint != null)
+ {
+ AddInternal(sampleOverrideDisplay = new SamplePointPiece(Item)
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.TopCentre
+ });
+ }
+ }
}
private void updateRepeats(IHasRepeats repeats)
@@ -331,39 +372,66 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return true;
}
+ private ScheduledDelegate dragOperation;
+
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
- OnDragHandled?.Invoke(e);
-
- if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time)
+ // schedule is temporary to ensure we don't process multiple times on a single update frame. we need to find a better method of doing this.
+ // without it, a hitobject's endtime may not always be in a valid state (ie. sliders, which needs to recompute their path).
+ dragOperation?.Cancel();
+ dragOperation = Scheduler.Add(() =>
{
- switch (hitObject)
+ OnDragHandled?.Invoke(e);
+
+ if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time)
{
- case IHasRepeats repeatHitObject:
- // find the number of repeats which can fit in the requested time.
- var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1);
- var proposedCount = Math.Max(0, (int)Math.Round((time - hitObject.StartTime) / lengthOfOneRepeat) - 1);
+ switch (hitObject)
+ {
+ case IHasRepeats repeatHitObject:
+ double proposedDuration = time - hitObject.StartTime;
- if (proposedCount == repeatHitObject.RepeatCount)
- return;
+ if (e.CurrentState.Keyboard.ShiftPressed)
+ {
+ if (hitObject.DifficultyControlPoint == DifficultyControlPoint.DEFAULT)
+ hitObject.DifficultyControlPoint = new DifficultyControlPoint();
- repeatHitObject.RepeatCount = proposedCount;
- beatmap.Update(hitObject);
- break;
+ var newVelocity = hitObject.DifficultyControlPoint.SliderVelocity * (repeatHitObject.Duration / proposedDuration);
- case IHasDuration endTimeHitObject:
- var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time));
+ if (Precision.AlmostEquals(newVelocity, hitObject.DifficultyControlPoint.SliderVelocity))
+ return;
- if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, beatmap.GetBeatLengthAtTime(snappedTime)))
- return;
+ hitObject.DifficultyControlPoint.SliderVelocity = newVelocity;
+ beatmap.Update(hitObject);
+ }
+ else
+ {
+ // find the number of repeats which can fit in the requested time.
+ var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1);
+ var proposedCount = Math.Max(0, (int)Math.Round(proposedDuration / lengthOfOneRepeat) - 1);
- endTimeHitObject.Duration = snappedTime - hitObject.StartTime;
- beatmap.Update(hitObject);
- break;
+ if (proposedCount == repeatHitObject.RepeatCount)
+ return;
+
+ repeatHitObject.RepeatCount = proposedCount;
+ beatmap.Update(hitObject);
+ }
+
+ break;
+
+ case IHasDuration endTimeHitObject:
+ var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time));
+
+ if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, beatmap.GetBeatLengthAtTime(snappedTime)))
+ return;
+
+ endTimeHitObject.Duration = snappedTime - hitObject.StartTime;
+ beatmap.Update(hitObject);
+ break;
+ }
}
- }
+ });
}
protected override void OnDragEnd(DragEndEvent e)
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 1170658abb..512226413b 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -162,7 +162,7 @@ namespace osu.Game.Screens.Edit
// todo: remove caching of this and consume via editorBeatmap?
dependencies.Cache(beatDivisor);
- AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin()));
+ AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin(), loadableBeatmap.BeatmapInfo));
dependencies.CacheAs(editorBeatmap);
changeHandler = new EditorChangeHandler(editorBeatmap);
dependencies.CacheAs(changeHandler);
@@ -333,10 +333,10 @@ namespace osu.Game.Screens.Edit
isNewBeatmap = false;
// apply any set-level metadata changes.
- beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet);
+ beatmapManager.Update(editorBeatmap.BeatmapInfo.BeatmapSet);
// save the loaded beatmap's data stream.
- beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin);
+ beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin);
updateLastSavedHash();
}
@@ -523,7 +523,10 @@ namespace osu.Game.Screens.Edit
var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo);
if (!(refetchedBeatmap is DummyWorkingBeatmap))
+ {
+ Logger.Log("Editor providing re-fetched beatmap post edit session");
Beatmap.Value = refetchedBeatmap;
+ }
return base.OnExiting(next);
}
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index 64eb6225fa..2e84ef437a 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@@ -44,6 +45,7 @@ namespace osu.Game.Screens.Edit
///
public readonly Bindable PlacementObject = new Bindable();
+ private readonly BeatmapInfo beatmapInfo;
public readonly IBeatmap PlayableBeatmap;
///
@@ -66,9 +68,37 @@ namespace osu.Game.Screens.Edit
private readonly Dictionary> startTimeBindables = new Dictionary>();
- public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null)
+ public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null, BeatmapInfo beatmapInfo = null)
{
PlayableBeatmap = playableBeatmap;
+
+ // ensure we are not working with legacy control points.
+ // if we leave the legacy points around they will be applied over any local changes on
+ // ApplyDefaults calls. this should eventually be removed once the default logic is moved to the decoder/converter.
+ if (PlayableBeatmap.ControlPointInfo is LegacyControlPointInfo)
+ {
+ var newControlPoints = new ControlPointInfo();
+
+ foreach (var controlPoint in PlayableBeatmap.ControlPointInfo.AllControlPoints)
+ {
+ switch (controlPoint)
+ {
+ case DifficultyControlPoint _:
+ case SampleControlPoint _:
+ // skip legacy types.
+ continue;
+
+ default:
+ newControlPoints.Add(controlPoint.Time, controlPoint);
+ break;
+ }
+ }
+
+ playableBeatmap.ControlPointInfo = newControlPoints;
+ }
+
+ this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo;
+
if (beatmapSkin is Skin skin)
BeatmapSkin = new EditorBeatmapSkin(skin);
@@ -80,11 +110,11 @@ namespace osu.Game.Screens.Edit
public BeatmapInfo BeatmapInfo
{
- get => PlayableBeatmap.BeatmapInfo;
- set => PlayableBeatmap.BeatmapInfo = value;
+ get => beatmapInfo;
+ set => throw new InvalidOperationException();
}
- public BeatmapMetadata Metadata => PlayableBeatmap.Metadata;
+ public BeatmapMetadata Metadata => beatmapInfo.Metadata;
public BeatmapDifficulty Difficulty
{
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index ba83261731..86e5729196 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -25,7 +25,9 @@ namespace osu.Game.Screens.Edit
public double TrackLength => track.Value?.Length ?? 60000;
- public ControlPointInfo ControlPointInfo;
+ public ControlPointInfo ControlPointInfo => Beatmap.ControlPointInfo;
+
+ public IBeatmap Beatmap { get; set; }
private readonly BindableBeatDivisor beatDivisor;
@@ -42,25 +44,15 @@ namespace osu.Game.Screens.Edit
///
public bool IsSeeking { get; private set; }
- public EditorClock(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
- : this(beatmap.ControlPointInfo, beatDivisor)
+ public EditorClock(IBeatmap beatmap = null, BindableBeatDivisor beatDivisor = null)
{
- }
+ Beatmap = beatmap ?? new Beatmap();
- public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor)
- {
- this.beatDivisor = beatDivisor;
-
- ControlPointInfo = controlPointInfo;
+ this.beatDivisor = beatDivisor ?? new BindableBeatDivisor();
underlyingClock = new DecoupleableInterpolatingFramedClock();
}
- public EditorClock()
- : this(new ControlPointInfo(), new BindableBeatDivisor())
- {
- }
-
///
/// Seek to the closest snappable beat from a time.
///
diff --git a/osu.Game/Screens/Edit/EditorRoundedScreen.cs b/osu.Game/Screens/Edit/EditorRoundedScreen.cs
index b271a145f5..508663224d 100644
--- a/osu.Game/Screens/Edit/EditorRoundedScreen.cs
+++ b/osu.Game/Screens/Edit/EditorRoundedScreen.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit
{
new Box
{
- Colour = ColourProvider.Dark4,
+ Colour = ColourProvider.Background3,
RelativeSizeAxes = Axes.Both,
},
roundedContent = new Container
diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
index 9e93b0b038..5bb40c09a5 100644
--- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs
+++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
@@ -10,7 +10,7 @@ using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Setup
{
- internal class MetadataSection : SetupSection
+ public class MetadataSection : SetupSection
{
protected LabelledTextBox ArtistTextBox;
protected LabelledTextBox RomanisedArtistTextBox;
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
index 48639789af..938c7f9cf0 100644
--- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
+++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
@@ -12,8 +12,6 @@ namespace osu.Game.Screens.Edit.Timing
{
new GroupSection(),
new TimingSection(),
- new DifficultySection(),
- new SampleSection(),
new EffectSection(),
};
}
diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
deleted file mode 100644
index 97d110c502..0000000000
--- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Game.Beatmaps.ControlPoints;
-
-namespace osu.Game.Screens.Edit.Timing
-{
- internal class DifficultySection : Section
- {
- private SliderWithTextBoxInput multiplierSlider;
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Flow.AddRange(new[]
- {
- multiplierSlider = new SliderWithTextBoxInput("Speed Multiplier")
- {
- Current = new DifficultyControlPoint().SpeedMultiplierBindable,
- KeyboardStep = 0.1f
- }
- });
- }
-
- protected override void OnControlPointChanged(ValueChangedEvent point)
- {
- if (point.NewValue != null)
- {
- var selectedPointBindable = point.NewValue.SpeedMultiplierBindable;
-
- // there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint).
- // generally that level of precision could only be set by externally editing the .osu file, so at the point
- // a user is looking to update this within the editor it should be safe to obliterate this additional precision.
- double expectedPrecision = new DifficultyControlPoint().SpeedMultiplierBindable.Precision;
- if (selectedPointBindable.Precision < expectedPrecision)
- selectedPointBindable.Precision = expectedPrecision;
-
- multiplierSlider.Current = selectedPointBindable;
- multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
- }
- }
-
- protected override DifficultyControlPoint CreatePoint()
- {
- var reference = Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time);
-
- return new DifficultyControlPoint
- {
- SpeedMultiplier = reference.SpeedMultiplier,
- };
- }
- }
-}
diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs
index 6d23b52c05..c8944d0357 100644
--- a/osu.Game/Screens/Edit/Timing/EffectSection.cs
+++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
@@ -13,13 +14,20 @@ namespace osu.Game.Screens.Edit.Timing
private LabelledSwitchButton kiai;
private LabelledSwitchButton omitBarLine;
+ private SliderWithTextBoxInput scrollSpeedSlider;
+
[BackgroundDependencyLoader]
private void load()
{
- Flow.AddRange(new[]
+ Flow.AddRange(new Drawable[]
{
kiai = new LabelledSwitchButton { Label = "Kiai Time" },
omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" },
+ scrollSpeedSlider = new SliderWithTextBoxInput("Scroll Speed")
+ {
+ Current = new EffectControlPoint().ScrollSpeedBindable,
+ KeyboardStep = 0.1f
+ }
});
}
@@ -32,6 +40,9 @@ namespace osu.Game.Screens.Edit.Timing
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
+
+ scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
+ scrollSpeedSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
}
}
@@ -42,7 +53,8 @@ namespace osu.Game.Screens.Edit.Timing
return new EffectControlPoint
{
KiaiMode = reference.KiaiMode,
- OmitFirstBarLine = reference.OmitFirstBarLine
+ OmitFirstBarLine = reference.OmitFirstBarLine,
+ ScrollSpeed = reference.ScrollSpeed,
};
}
}
diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs
index 7b553ac7ad..a8de476d67 100644
--- a/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs
+++ b/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
public DifficultyRowAttribute(DifficultyControlPoint difficulty)
: base(difficulty, "difficulty")
{
- speedMultiplier = difficulty.SpeedMultiplierBindable.GetBoundCopy();
+ speedMultiplier = difficulty.SliderVelocityBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
@@ -32,7 +32,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
},
text = new AttributeText(Point)
{
- Width = 40,
+ Width = 45,
},
});
diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs
index 812407d6da..1b33fd62aa 100644
--- a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs
+++ b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs
@@ -12,14 +12,18 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
{
private readonly Bindable kiaiMode;
private readonly Bindable omitBarLine;
+ private readonly BindableNumber scrollSpeed;
+
private AttributeText kiaiModeBubble;
private AttributeText omitBarLineBubble;
+ private AttributeText text;
public EffectRowAttribute(EffectControlPoint effect)
: base(effect, "effect")
{
kiaiMode = effect.KiaiModeBindable.GetBoundCopy();
omitBarLine = effect.OmitFirstBarLineBindable.GetBoundCopy();
+ scrollSpeed = effect.ScrollSpeedBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
@@ -27,12 +31,20 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
{
Content.AddRange(new Drawable[]
{
+ new AttributeProgressBar(Point)
+ {
+ Current = scrollSpeed,
+ },
+ text = new AttributeText(Point) { Width = 45 },
kiaiModeBubble = new AttributeText(Point) { Text = "kiai" },
omitBarLineBubble = new AttributeText(Point) { Text = "no barline" },
});
kiaiMode.BindValueChanged(enabled => kiaiModeBubble.FadeTo(enabled.NewValue ? 1 : 0), true);
omitBarLine.BindValueChanged(enabled => omitBarLineBubble.FadeTo(enabled.NewValue ? 1 : 0), true);
+ scrollSpeed.BindValueChanged(_ => updateText(), true);
}
+
+ private void updateText() => text.Text = $"{scrollSpeed.Value:n2}x";
}
}
diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs
deleted file mode 100644
index 52709a2bbe..0000000000
--- a/osu.Game/Screens/Edit/Timing/SampleSection.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics.UserInterfaceV2;
-
-namespace osu.Game.Screens.Edit.Timing
-{
- internal class SampleSection : Section
- {
- private LabelledTextBox bank;
- private SliderWithTextBoxInput volume;
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Flow.AddRange(new Drawable[]
- {
- bank = new LabelledTextBox
- {
- Label = "Bank Name",
- },
- volume = new SliderWithTextBoxInput("Volume")
- {
- Current = new SampleControlPoint().SampleVolumeBindable,
- }
- });
- }
-
- protected override void OnControlPointChanged(ValueChangedEvent point)
- {
- if (point.NewValue != null)
- {
- bank.Current = point.NewValue.SampleBankBindable;
- bank.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
-
- volume.Current = point.NewValue.SampleVolumeBindable;
- volume.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
- }
- }
-
- protected override SampleControlPoint CreatePoint() => new SampleControlPoint(); // TODO: remove
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
index 97377278a6..abda9e897b 100644
--- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
@@ -150,6 +150,11 @@ namespace osu.Game.Screens.OnlinePlay.Components
notifyRoomsUpdated();
}
- private void notifyRoomsUpdated() => Scheduler.AddOnce(() => RoomsUpdated?.Invoke());
+ private void notifyRoomsUpdated()
+ {
+ Scheduler.AddOnce(invokeRoomsUpdated);
+
+ void invokeRoomsUpdated() => RoomsUpdated?.Invoke();
+ }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index 80a5daa7c8..0edf5dde6d 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -7,7 +7,6 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -99,14 +98,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex(@"28242d"),
+ Colour = colourProvider.Background4
},
new GridContainer
{
@@ -249,7 +248,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f),
+ Colour = colourProvider.Background5
},
new FillFlowContainer
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
index 6c3dfe7382..cf1066df10 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
@@ -79,11 +79,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void load()
{
isConnected.BindTo(client.IsConnected);
- isConnected.BindValueChanged(c => Scheduler.AddOnce(() =>
- {
- if (isConnected.Value && IsLoaded)
- PollImmediately();
- }), true);
+ isConnected.BindValueChanged(c => Scheduler.AddOnce(poll), true);
+ }
+
+ private void poll()
+ {
+ if (isConnected.Value && IsLoaded)
+ PollImmediately();
}
protected override Task Poll()
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
index 0f256160eb..a380ddef25 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
@@ -19,15 +19,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
- Client.RoomUpdated += OnRoomUpdated;
-
- Client.UserLeft += UserLeft;
- Client.UserKicked += UserKicked;
- Client.UserJoined += UserJoined;
+ Client.RoomUpdated += invokeOnRoomUpdated;
+ Client.UserLeft += invokeUserLeft;
+ Client.UserKicked += invokeUserKicked;
+ Client.UserJoined += invokeUserJoined;
OnRoomUpdated();
}
+ private void invokeOnRoomUpdated() => Scheduler.AddOnce(OnRoomUpdated);
+ private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.AddOnce(UserJoined, user);
+ private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(UserKicked, user);
+ private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.AddOnce(UserLeft, user);
+
///
/// Invoked when a user has joined the room.
///
@@ -63,10 +67,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
if (Client != null)
{
- Client.UserLeft -= UserLeft;
- Client.UserKicked -= UserKicked;
- Client.UserJoined -= UserJoined;
- Client.RoomUpdated -= OnRoomUpdated;
+ Client.RoomUpdated -= invokeOnRoomUpdated;
+ Client.UserLeft -= invokeUserLeft;
+ Client.UserKicked -= invokeUserKicked;
+ Client.UserJoined -= invokeUserJoined;
}
base.Dispose(isDisposing);
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
index 7384c60888..9e000aa712 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Specialized;
using Humanizer;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -77,14 +76,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex(@"28242d"),
+ Colour = colourProvider.Background4
},
new GridContainer
{
@@ -256,7 +255,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f),
+ Colour = colourProvider.Background5
},
new FillFlowContainer
{
diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs
index ea158c5789..2a1c4599d5 100644
--- a/osu.Game/Screens/Play/FailAnimation.cs
+++ b/osu.Game/Screens/Play/FailAnimation.cs
@@ -6,11 +6,13 @@ using osu.Framework.Bindables;
using osu.Game.Rulesets.UI;
using System;
using System.Collections.Generic;
+using ManagedBass.Fx;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Audio.Effects;
using osu.Game.Beatmaps;
@@ -24,25 +26,38 @@ namespace osu.Game.Screens.Play
/// Manage the animation to be applied when a player fails.
/// Single use and automatically disposed after use.
///
- public class FailAnimation : CompositeDrawable
+ public class FailAnimation : Container
{
public Action OnComplete;
private readonly DrawableRuleset drawableRuleset;
-
private readonly BindableDouble trackFreq = new BindableDouble(1);
+ private Container filters;
+
+ private Box 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]
@@ -51,7 +66,26 @@ namespace osu.Game.Screens.Play
track = beatmap.Value.Track;
failSample = audio.Samples.Get(@"Gameplay/failsound");
- AddInternal(failLowPassFilter = new AudioFilter(audio.TrackMixer));
+ AddRangeInternal(new Drawable[]
+ {
+ filters = new Container
+ {
+ Children = new Drawable[]
+ {
+ failLowPassFilter = new AudioFilter(audio.TrackMixer),
+ failHighPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
+ },
+ },
+ Content,
+ failFlash = new Box
+ {
+ Colour = Color4.Red,
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ Depth = float.MinValue,
+ Alpha = 0
+ },
+ });
}
private bool started;
@@ -66,21 +100,42 @@ namespace osu.Game.Screens.Play
started = true;
- failSample.Play();
-
this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ =>
{
OnComplete?.Invoke();
- Expire();
});
+ failHighPassFilter.CutoffTo(300);
failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic);
+ failSample.Play();
track.AddAdjustment(AdjustableProperty.Frequency, trackFreq);
applyToPlayfield(drawableRuleset.Playfield);
- drawableRuleset.Playfield.HitObjectContainer.FlashColour(Color4.Red, 500);
drawableRuleset.Playfield.HitObjectContainer.FadeOut(duration / 2);
+
+ 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()
@@ -129,11 +184,5 @@ namespace osu.Game.Screens.Play
obj.MoveTo(originalPosition + new Vector2(0, 400), duration);
}
}
-
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
- track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
- }
}
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 444bea049b..5398a955b3 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -230,17 +230,53 @@ namespace osu.Game.Screens.Play
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
GameplayClockContainer.Add(rulesetSkinProvider);
- rulesetSkinProvider.AddRange(new[]
+ rulesetSkinProvider.AddRange(new Drawable[]
{
- // underlay and gameplay should have access to the skinning sources.
- createUnderlayComponents(),
- createGameplayComponents(Beatmap.Value, playableBeatmap)
+ failAnimationLayer = new FailAnimation(DrawableRuleset)
+ {
+ OnComplete = onFailComplete,
+ Children = new[]
+ {
+ // underlay and gameplay should have access to the skinning sources.
+ createUnderlayComponents(),
+ createGameplayComponents(Beatmap.Value, playableBeatmap)
+ }
+ },
+ FailOverlay = new FailOverlay
+ {
+ OnRetry = Restart,
+ OnQuit = () => PerformExit(true),
+ },
+ new HotkeyExitOverlay
+ {
+ Action = () =>
+ {
+ if (!this.IsCurrentScreen()) return;
+
+ fadeOut(true);
+ PerformExit(false);
+ },
+ },
});
+ if (Configuration.AllowRestart)
+ {
+ rulesetSkinProvider.Add(new HotkeyRetryOverlay
+ {
+ Action = () =>
+ {
+ if (!this.IsCurrentScreen()) return;
+
+ fadeOut(true);
+ Restart();
+ },
+ });
+ }
+
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
// also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
- rulesetSkinProvider.Add(createOverlayComponents(Beatmap.Value));
+ failAnimationLayer.Add(createOverlayComponents(Beatmap.Value));
if (!DrawableRuleset.AllowGameplayOverlays)
{
@@ -375,11 +411,6 @@ namespace osu.Game.Screens.Play
RequestSkip = () => progressToResults(false),
Alpha = 0
},
- FailOverlay = new FailOverlay
- {
- OnRetry = Restart,
- OnQuit = () => PerformExit(true),
- },
PauseOverlay = new PauseOverlay
{
OnResume = Resume,
@@ -387,18 +418,7 @@ namespace osu.Game.Screens.Play
OnRetry = Restart,
OnQuit = () => PerformExit(true),
},
- new HotkeyExitOverlay
- {
- Action = () =>
- {
- if (!this.IsCurrentScreen()) return;
-
- fadeOut(true);
- PerformExit(false);
- },
- },
- failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
- }
+ },
};
if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays)
@@ -410,20 +430,6 @@ namespace osu.Game.Screens.Play
if (GameplayClockContainer is MasterGameplayClockContainer master)
HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate;
- if (Configuration.AllowRestart)
- {
- container.Add(new HotkeyRetryOverlay
- {
- Action = () =>
- {
- if (!this.IsCurrentScreen()) return;
-
- fadeOut(true);
- Restart();
- },
- });
- }
-
return container;
}
@@ -541,7 +547,7 @@ namespace osu.Game.Screens.Play
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
if (ValidForResume && HasFailed)
{
- failAnimation.FinishTransforms(true);
+ failAnimationLayer.FinishTransforms(true);
return;
}
@@ -766,7 +772,7 @@ namespace osu.Game.Screens.Play
protected FailOverlay FailOverlay { get; private set; }
- private FailAnimation failAnimation;
+ private FailAnimation failAnimationLayer;
private bool onFail()
{
@@ -782,7 +788,7 @@ namespace osu.Game.Screens.Play
if (PauseOverlay.State.Value == Visibility.Visible)
PauseOverlay.Hide();
- failAnimation.Start();
+ failAnimationLayer.Start();
if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail))
Restart();
@@ -956,7 +962,7 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(IScreen next)
{
screenSuspension?.RemoveAndDisposeImmediately();
- failAnimation?.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/PlayerSettings/PlayerCheckbox.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs
index 8a4acacb24..26887327cd 100644
--- a/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
{
Nub.AccentColour = colours.Yellow;
Nub.GlowingAccentColour = colours.YellowLighter;
- Nub.GlowColour = colours.YellowDarker;
+ Nub.GlowColour = colours.YellowDark;
}
}
}
diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs
index c8e281195a..216e46d429 100644
--- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
AccentColour = colours.Yellow;
Nub.AccentColour = colours.Yellow;
Nub.GlowingAccentColour = colours.YellowLighter;
- Nub.GlowColour = colours.YellowDarker;
+ Nub.GlowColour = colours.YellowDark;
}
}
}
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 6cafcb9d16..a2dea355ac 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -410,7 +410,7 @@ namespace osu.Game.Screens.Select
{
if (e.NewValue is DummyWorkingBeatmap || !this.IsCurrentScreen()) return;
- Logger.Log($"working beatmap updated to {e.NewValue}");
+ Logger.Log($"Song select working beatmap updated to {e.NewValue}");
if (!Carousel.SelectBeatmap(e.NewValue.BeatmapInfo, false))
{
diff --git a/osu.Game/Stores/RealmRulesetStore.cs b/osu.Game/Stores/RealmRulesetStore.cs
new file mode 100644
index 0000000000..27eb5d797f
--- /dev/null
+++ b/osu.Game/Stores/RealmRulesetStore.cs
@@ -0,0 +1,263 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using osu.Framework;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Game.Database;
+using osu.Game.Models;
+using osu.Game.Rulesets;
+
+#nullable enable
+
+namespace osu.Game.Stores
+{
+ public class RealmRulesetStore : IDisposable
+ {
+ private readonly RealmContextFactory realmFactory;
+
+ private const string ruleset_library_prefix = @"osu.Game.Rulesets";
+
+ private readonly Dictionary loadedAssemblies = new Dictionary();
+
+ ///
+ /// All available rulesets.
+ ///
+ public IEnumerable AvailableRulesets => availableRulesets;
+
+ private readonly List availableRulesets = new List();
+
+ public RealmRulesetStore(RealmContextFactory realmFactory, Storage? storage = null)
+ {
+ this.realmFactory = realmFactory;
+
+ // On android in release configuration assemblies are loaded from the apk directly into memory.
+ // We cannot read assemblies from cwd, so should check loaded assemblies instead.
+ loadFromAppDomain();
+
+ // This null check prevents Android from attempting to load the rulesets from disk,
+ // as the underlying path "AppContext.BaseDirectory", despite being non-nullable, it returns null on android.
+ // See https://github.com/xamarin/xamarin-android/issues/3489.
+ if (RuntimeInfo.StartupDirectory != null)
+ loadFromDisk();
+
+ // the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory.
+ // It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail
+ // to load as unable to locate the game core assembly.
+ AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly;
+
+ var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
+ if (rulesetStorage != null)
+ loadUserRulesets(rulesetStorage);
+
+ addMissingRulesets();
+ }
+
+ ///
+ /// Retrieve a ruleset using a known ID.
+ ///
+ /// The ruleset's internal ID.
+ /// A ruleset, if available, else null.
+ public IRulesetInfo? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id);
+
+ ///
+ /// Retrieve a ruleset using a known short name.
+ ///
+ /// The ruleset's short name.
+ /// A ruleset, if available, else null.
+ public IRulesetInfo? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName);
+
+ private Assembly? resolveRulesetDependencyAssembly(object? sender, ResolveEventArgs args)
+ {
+ var asm = new AssemblyName(args.Name);
+
+ // the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies.
+ // this attempts resolving the ruleset dependencies on game core and framework assemblies by returning assemblies with the same assembly name
+ // already loaded in the AppDomain.
+ var domainAssembly = AppDomain.CurrentDomain.GetAssemblies()
+ // Given name is always going to be equally-or-more qualified than the assembly name.
+ .Where(a =>
+ {
+ string? name = a.GetName().Name;
+ if (name == null)
+ return false;
+
+ return args.Name.Contains(name, StringComparison.Ordinal);
+ })
+ // Pick the greatest assembly version.
+ .OrderByDescending(a => a.GetName().Version)
+ .FirstOrDefault();
+
+ if (domainAssembly != null)
+ return domainAssembly;
+
+ return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName);
+ }
+
+ private void addMissingRulesets()
+ {
+ realmFactory.Context.Write(realm =>
+ {
+ var rulesets = realm.All();
+
+ List instances = loadedAssemblies.Values
+ .Select(r => Activator.CreateInstance(r) as Ruleset)
+ .Where(r => r != null)
+ .Select(r => r.AsNonNull())
+ .ToList();
+
+ // add all legacy rulesets first to ensure they have exclusive choice of primary key.
+ foreach (var r in instances.Where(r => r is ILegacyRuleset))
+ {
+ if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.ID) == null)
+ realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.ID));
+ }
+
+ // add any other rulesets which have assemblies present but are not yet in the database.
+ foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
+ {
+ if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
+ {
+ var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
+
+ if (existingSameShortName != null)
+ {
+ // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
+ // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
+ // in such cases, update the instantiation info of the existing entry to point to the new one.
+ existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
+ }
+ else
+ realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.ID));
+ }
+ }
+
+ List detachedRulesets = new List();
+
+ // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage.
+ foreach (var r in rulesets)
+ {
+ try
+ {
+ var type = Type.GetType(r.InstantiationInfo);
+
+ if (type == null)
+ throw new InvalidOperationException(@"Type resolution failure.");
+
+ var rInstance = (Activator.CreateInstance(type) as Ruleset)?.RulesetInfo;
+
+ if (rInstance == null)
+ throw new InvalidOperationException(@"Instantiation failure.");
+
+ r.Name = rInstance.Name;
+ r.ShortName = rInstance.ShortName;
+ r.InstantiationInfo = rInstance.InstantiationInfo;
+ r.Available = true;
+
+ detachedRulesets.Add(r.Clone());
+ }
+ catch (Exception ex)
+ {
+ r.Available = false;
+ Logger.Log($"Could not load ruleset {r}: {ex.Message}");
+ }
+ }
+
+ availableRulesets.AddRange(detachedRulesets);
+ });
+ }
+
+ private void loadFromAppDomain()
+ {
+ foreach (var ruleset in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ string? rulesetName = ruleset.GetName().Name;
+
+ if (rulesetName == null)
+ continue;
+
+ if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || rulesetName.Contains(@"Tests"))
+ continue;
+
+ addRuleset(ruleset);
+ }
+ }
+
+ private void loadUserRulesets(Storage rulesetStorage)
+ {
+ var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll");
+
+ foreach (var ruleset in rulesets.Where(f => !f.Contains(@"Tests")))
+ loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
+ }
+
+ private void loadFromDisk()
+ {
+ try
+ {
+ var files = Directory.GetFiles(RuntimeInfo.StartupDirectory, @$"{ruleset_library_prefix}.*.dll");
+
+ foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests")))
+ loadRulesetFromFile(file);
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, $"Could not load rulesets from directory {RuntimeInfo.StartupDirectory}");
+ }
+ }
+
+ private void loadRulesetFromFile(string file)
+ {
+ var filename = Path.GetFileNameWithoutExtension(file);
+
+ if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
+ return;
+
+ try
+ {
+ addRuleset(Assembly.LoadFrom(file));
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, $"Failed to load ruleset {filename}");
+ }
+ }
+
+ private void addRuleset(Assembly assembly)
+ {
+ if (loadedAssemblies.ContainsKey(assembly))
+ return;
+
+ // the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799).
+ // as a failsafe, also compare by FullName.
+ if (loadedAssemblies.Any(a => a.Key.FullName == assembly.FullName))
+ return;
+
+ try
+ {
+ loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset)));
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, $"Failed to add ruleset {assembly}");
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly;
+ }
+ }
+}
diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs
index 34393fba7d..c2e9892735 100644
--- a/osu.Game/Tests/Visual/EditorClockTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Visual
@@ -23,7 +22,7 @@ namespace osu.Game.Tests.Visual
protected EditorClockTestScene()
{
- Clock = new EditorClock(new ControlPointInfo(), BeatDivisor) { IsCoupled = false };
+ Clock = new EditorClock(new Beatmap(), BeatDivisor) { IsCoupled = false };
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@@ -44,7 +43,7 @@ namespace osu.Game.Tests.Visual
private void beatmapChanged(ValueChangedEvent e)
{
- Clock.ControlPointInfo = e.NewValue.Beatmap.ControlPointInfo;
+ Clock.Beatmap = e.NewValue.Beatmap;
Clock.ChangeSource(e.NewValue.Track);
Clock.ProcessFrame();
}
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 2c0ca0b872..5e4e5942d9 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -53,7 +53,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
public MultiplayerRoomUser AddUser(User user, bool markAsPlaying = false)
{
var roomUser = new MultiplayerRoomUser(user.Id) { User = user };
- ((IMultiplayerClient)this).UserJoined(roomUser);
+
+ addUser(roomUser);
if (markAsPlaying)
PlayingUserIds.Add(user.Id);
@@ -61,7 +62,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
return roomUser;
}
- public void AddNullUser() => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(TestUserLookupCache.NULL_USER_ID));
+ public void AddNullUser() => addUser(new MultiplayerRoomUser(TestUserLookupCache.NULL_USER_ID));
+
+ private void addUser(MultiplayerRoomUser user)
+ {
+ ((IMultiplayerClient)this).UserJoined(user).Wait();
+
+ // We want the user to be immediately available for testing, so force a scheduler update to run the update-bound continuation.
+ Scheduler.Update();
+ }
public void RemoveUser(User user)
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index c6121ddd5f..32d6eeab29 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,11 +36,12 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 110de79285..92abab036a 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,8 +70,8 @@
-
-
+
+
@@ -93,7 +93,7 @@
-
+