diff --git a/osu.Android.props b/osu.Android.props
index 4b89e82729..3ede0b85da 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -11,7 +11,7 @@
manifestmerger.jar
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs
index 18d3d29bdc..2426f8c886 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
- AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
+ AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault);
}
[Test]
@@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addPlacementSteps(times, positions);
addPathCheckStep(times, positions);
- AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
+ AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
}
[Test]
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
index f25b66c360..beba5811fe 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
@@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
double[] times = { 100, 300 };
float[] positions = { 200, 300 };
addBlueprintStep(times, positions);
- AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
+ AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400);
- AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
+ AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
}
[Test]
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 7774a7da09..2c8ef9eae0 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
var xPositionData = obj as IHasXPosition;
var yPositionData = obj as IHasYPosition;
var comboData = obj as IHasCombo;
+ var sliderVelocityData = obj as IHasSliderVelocity;
switch (obj)
{
@@ -41,7 +42,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
- LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
+ LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
+ SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
}.Yield();
case IHasDuration endTime:
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
index 74d6565600..7a577f8a83 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
@@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateHitObjectFromPath(JuiceStream hitObject)
{
// The SV setting may need to be changed for the current path.
- var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable;
+ var svBindable = hitObject.SliderVelocityBindable;
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
double requiredVelocity = path.ComputeRequiredVelocity();
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index ea5f54a775..cd8894753f 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Catch.Edit
private void load()
{
// todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation.
- RightSideToolboxContainer.Alpha = 0;
DistanceSpacingMultiplier.Disabled = true;
LayerBelowRuleset.Add(new PlayfieldBorder
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs
index cae19e9468..180cb98ed7 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
index 57c06e1cd1..83db9f665b 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs
index ce06b841aa..3afb8c3d89 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
index 9e38913be7..c537897439 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModNightcore : ModNightcore
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs
index af03c9acab..4c66c054e1 100644
--- a/osu.Game.Rulesets.Catch/Objects/Banana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs
@@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Catch.Objects
public override Judgement CreateJudgement() => new CatchBananaJudgement();
- private static readonly List samples = new List { new BananaHitSampleInfo() };
+ private static readonly IList default_banana_samples = new List { new BananaHitSampleInfo() }.AsReadOnly();
public Banana()
{
- Samples = samples;
+ Samples = default_banana_samples;
}
// override any external colour changes with banananana
@@ -47,13 +47,13 @@ namespace osu.Game.Rulesets.Catch.Objects
}
}
- private class BananaHitSampleInfo : HitSampleInfo, IEquatable
+ public class BananaHitSampleInfo : HitSampleInfo, IEquatable
{
private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" };
public override IEnumerable LookupNames => lookup_names;
- public BananaHitSampleInfo(int volume = 0)
+ public BananaHitSampleInfo(int volume = 100)
: base(string.Empty, volume: volume)
{
}
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index b45f95a8e6..5bd4ac86f5 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -1,7 +1,9 @@
// 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.Threading;
+using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
@@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
StartTime = time,
BananaIndex = i,
+ Samples = new List { new Banana.BananaHitSampleInfo(GetSampleInfo().Volume) }
});
time += spacing;
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 96e2d5c4e5..169e99c90c 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Newtonsoft.Json;
+using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -16,7 +17,7 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class JuiceStream : CatchHitObject, IHasPathWithRepeats
+ public class JuiceStream : CatchHitObject, IHasPathWithRepeats, IHasSliderVelocity
{
///
/// Positional distance that results in a duration of one second, before any speed adjustments.
@@ -27,6 +28,19 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; }
+ public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1)
+ {
+ Precision = 0.01,
+ MinValue = 0.1,
+ MaxValue = 10
+ };
+
+ public double SliderVelocity
+ {
+ get => SliderVelocityBindable.Value;
+ set => SliderVelocityBindable.Value = value;
+ }
+
[JsonIgnore]
private double velocityFactor;
@@ -34,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
private double tickDistanceFactor;
[JsonIgnore]
- public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
+ public double Velocity => velocityFactor * SliderVelocity;
[JsonIgnore]
- public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
+ public double TickDistance => tickDistanceFactor * SliderVelocity;
///
/// The length of one span of this .
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index ab754e51f7..f77dab56c8 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -418,10 +418,13 @@ namespace osu.Game.Rulesets.Catch.UI
private void clearPlate(DroppedObjectAnimation animation)
{
- var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray();
+ var caughtObjects = caughtObjectContainer.Children.ToArray();
caughtObjectContainer.Clear(false);
+ // Use the already returned PoolableDrawables for new objects
+ var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray();
+
droppedObjectTarget.AddRange(droppedObjects);
foreach (var droppedObject in droppedObjects)
@@ -430,10 +433,10 @@ namespace osu.Game.Rulesets.Catch.UI
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
{
- var droppedObject = getDroppedObject(caughtObject);
-
caughtObjectContainer.Remove(caughtObject, false);
+ var droppedObject = getDroppedObject(caughtObject);
+
droppedObjectTarget.Add(droppedObject);
applyDropAnimation(droppedObject, animation);
@@ -456,6 +459,8 @@ namespace osu.Game.Rulesets.Catch.UI
break;
}
+ // Define lifetime start for dropped objects to be disposed correctly when rewinding replay
+ d.LifetimeStart = Clock.CurrentTime;
d.Expire();
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 2bdd0e16ad..91b7be6e8f 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Beatmaps.Formats;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
@@ -49,15 +48,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
- DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
double beatLength;
-#pragma warning disable 618
- if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
-#pragma warning restore 618
- beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
+ if (hitObject.LegacyBpmMultiplier.HasValue)
+ beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
+ else if (hitObject is IHasSliderVelocity hasSliderVelocity)
+ beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
else
- beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
+ beatLength = timingPoint.BeatLength;
SpanCount = repeatsData?.SpanCount() ?? 1;
StartTime = (int)Math.Round(hitObject.StartTime);
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs
index bec0a6a1d3..309393b664 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.5;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
index a302f95966..f4b9cf3b88 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => 1;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs
index 014954dd60..8d48e3acde 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.5;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
index 4cc712060c..748725af9f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModNightcore : ModNightcore
{
- public override double ScoreMultiplier => 1;
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 6e1c6cf80f..372ef1e164 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -350,13 +350,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
- if (HitObject.SampleControlPoint == null)
- {
- throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
- + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
- }
-
- slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray();
+ slidingSample.Samples = HitObject.CreateSlidingSamples().Cast().ToArray();
}
public override void StopAllSamples()
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
index 6f1d45ad8c..4d298bb671 100644
--- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
@@ -10,7 +10,7 @@
["Gameplay/soft-hitnormal"],
["Gameplay/drum-hitnormal"]
],
- "Samples": ["Gameplay/-hitnormal"]
+ "Samples": ["Gameplay/normal-hitnormal"]
}, {
"StartTime": 1875.0,
"EndTime": 2750.0,
@@ -19,7 +19,7 @@
["Gameplay/soft-hitnormal"],
["Gameplay/drum-hitnormal"]
],
- "Samples": ["Gameplay/-hitnormal"]
+ "Samples": ["Gameplay/normal-hitnormal"]
}]
}, {
"StartTime": 3750.0,
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
index e7ac38c20e..b05c755bfd 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
@@ -138,8 +138,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
- && mergedSlider.Samples.SequenceEqual(slider1.Samples)
- && mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint);
+ && mergedSlider.Samples.SequenceEqual(slider1.Samples);
});
AddAssert("slider end is at same completion for last slider", () =>
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
index 6cb77c7b92..a104433ea9 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
@@ -181,10 +181,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
if (slider is null) return;
- slider.SampleControlPoint.SampleBank = "soft";
- slider.SampleControlPoint.SampleVolume = 70;
- sample = new HitSampleInfo("hitwhistle");
- slider.Samples.Add(sample);
+ sample = new HitSampleInfo("hitwhistle", "soft", volume: 70);
+ slider.Samples.Add(sample.With());
});
AddStep("select added slider", () =>
@@ -207,9 +205,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("sliders have hitsounds", hasHitsounds);
bool hasHitsounds() => sample is not null &&
- EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" &&
- o.SampleControlPoint.SampleVolume == 70 &&
- o.Samples.Contains(sample));
+ EditorBeatmap.HitObjects.All(o => o.Samples.Contains(sample));
}
private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints)
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
index 53465d43c9..a162d9a491 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
@@ -199,8 +199,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Precision.AlmostEquals(circle.StartTime, time, 1)
&& Precision.AlmostEquals(circle.Position, position, 0.01f)
&& circle.NewCombo == startsNewCombo
- && circle.Samples.SequenceEqual(slider.HeadCircle.Samples)
- && circle.SampleControlPoint.IsRedundant(slider.SampleControlPoint);
+ && circle.Samples.SequenceEqual(slider.HeadCircle.Samples);
}
private bool sliderRestored(Slider slider)
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModBubbles.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModBubbles.cs
new file mode 100644
index 0000000000..e72a1f79f5
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModBubbles.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public partial class TestSceneOsuModBubbles : OsuModTestScene
+ {
+ [Test]
+ public void TestOsuModBubbles() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModBubbles(),
+ Autoplay = true,
+ PassCondition = () => true
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png
new file mode 100644
index 0000000000..258162c486
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 907422858e..c84a6ab70f 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -18,6 +19,7 @@ using osu.Framework.Testing.Input;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
@@ -40,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable background;
+ private readonly Bindable ripples = new Bindable();
+
public TestSceneGameplayCursor()
{
var ruleset = new OsuRuleset();
@@ -57,6 +61,8 @@ namespace osu.Game.Rulesets.Osu.Tests
});
});
+ AddToggleStep("ripples", v => ripples.Value = v);
+
AddSliderStep("circle size", 0f, 10f, 0f, val =>
{
config.SetValue(OsuSetting.AutoCursorSize, true);
@@ -67,6 +73,13 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("test cursor container", () => loadContent(false));
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var rulesetConfig = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
+ rulesetConfig.BindWith(OsuRulesetSetting.ShowCursorRipples, ripples);
+ }
+
[TestCase(1, 1)]
[TestCase(5, 1)]
[TestCase(10, 1)]
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
index 5d9316a21b..ee70441688 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
@@ -439,7 +439,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
- DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
+ SliderVelocity = 0.1f;
DefaultsApplied += _ =>
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs
index a32f0a13b8..fc2e6d1f72 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs
@@ -7,7 +7,6 @@ using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
- DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = velocity },
+ SliderVelocity = velocity,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index 5f27cdc191..d83926ab9b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -8,7 +8,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@@ -350,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
- DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f },
+ SliderVelocity = 0.1f,
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
index 29e6fc4301..f4257a9ee7 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
@@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
- DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
+ SliderVelocity = 0.1f;
DefaultsApplied += _ =>
{
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
index e9518895be..d03ee81f0d 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
@@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
var positionData = original as IHasPosition;
var comboData = original as IHasCombo;
+ var sliderVelocityData = original as IHasSliderVelocity;
+ var generateTicksData = original as IHasGenerateTicks;
switch (original)
{
@@ -47,7 +49,9 @@ 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 h is Slider && h.GetEndTime() < HitObject.StartTime)?
- .DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
+ double? nearestSliderVelocity = (editorBeatmap.HitObjects
+ .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity;
- HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
+ HitObject.SliderVelocity = nearestSliderVelocity ?? 1;
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index e444287b73..6685507ee0 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -14,7 +14,6 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@@ -311,17 +310,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var splitControlPoints = controlPoints.Take(index + 1).ToList();
controlPoints.RemoveRange(0, index);
- // Turn the control points which were split off into a new slider.
- var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
- var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone();
-
var newSlider = new Slider
{
StartTime = HitObject.StartTime,
Position = HitObject.Position + splitControlPoints[0].Position,
NewCombo = HitObject.NewCombo,
- SampleControlPoint = samplePoint,
- DifficultyControlPoint = difficultyPoint,
LegacyLastTickOffset = HitObject.LegacyLastTickOffset,
Samples = HitObject.Samples.Select(s => s.With()).ToList(),
RepeatCount = HitObject.RepeatCount,
@@ -378,15 +371,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
- var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
- samplePoint.Time = time;
-
editorBeatmap.Add(new HitCircle
{
StartTime = time,
Position = position,
NewCombo = i == 0 && HitObject.NewCombo,
- SampleControlPoint = samplePoint,
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
});
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 7a70257f3a..ff1e208186 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -13,8 +13,8 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Utils;
using osu.Framework.Input.Events;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
@@ -62,7 +62,12 @@ namespace osu.Game.Rulesets.Osu.Edit
private void load()
{
// Give a bit of breathing room around the playfield content.
- PlayfieldContentContainer.Padding = new MarginPadding(10);
+ PlayfieldContentContainer.Padding = new MarginPadding
+ {
+ Vertical = 10,
+ Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10,
+ Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10,
+ };
LayerBelowRuleset.AddRange(new Drawable[]
{
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 6d5280e528..2a6d6ce4c3 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -362,7 +362,6 @@ namespace osu.Game.Rulesets.Osu.Edit
StartTime = firstHitObject.StartTime,
Position = firstHitObject.Position,
NewCombo = firstHitObject.NewCombo,
- SampleControlPoint = firstHitObject.SampleControlPoint,
Samples = firstHitObject.Samples,
};
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
index 9e71f657ce..2394cf92fc 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -10,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModBarrelRoll : ModBarrelRoll, IApplicableToDrawableHitObject
{
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModBubbles) };
+
public void ApplyToDrawableHitObject(DrawableHitObject d)
{
d.OnUpdate += _ =>
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs
new file mode 100644
index 0000000000..12e2090f89
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs
@@ -0,0 +1,214 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Pooling;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Scoring;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public partial class OsuModBubbles : Mod, IApplicableToDrawableRuleset, IApplicableToDrawableHitObject, IApplicableToScoreProcessor
+ {
+ public override string Name => "Bubbles";
+
+ public override string Acronym => "BU";
+
+ public override LocalisableString Description => "Don't let their popping distract you!";
+
+ public override double ScoreMultiplier => 1;
+
+ public override ModType Type => ModType.Fun;
+
+ // Compatibility with these seems potentially feasible in the future, blocked for now because they don't work as one would expect
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModBarrelRoll), typeof(OsuModMagnetised), typeof(OsuModRepel) };
+
+ private PlayfieldAdjustmentContainer bubbleContainer = null!;
+
+ private readonly Bindable currentCombo = new BindableInt();
+
+ private float maxSize;
+ private float bubbleSize;
+ private double bubbleFade;
+
+ private readonly DrawablePool bubblePool = new DrawablePool(100);
+
+ public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
+
+ public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
+ {
+ currentCombo.BindTo(scoreProcessor.Combo);
+ currentCombo.BindValueChanged(combo =>
+ maxSize = Math.Min(1.75f, (float)(1.25 + 0.005 * combo.NewValue)), true);
+ }
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ // Multiplying by 2 results in an initial size that is too large, hence 1.90 has been chosen
+ // Also avoids the HitObject bleeding around the edges of the bubble drawable at minimum size
+ bubbleSize = (float)(drawableRuleset.Beatmap.HitObjects.OfType().First().Radius * 1.90f);
+ bubbleFade = drawableRuleset.Beatmap.HitObjects.OfType().First().TimePreempt * 2;
+
+ // We want to hide the judgements since they are obscured by the BubbleDrawable (due to layering)
+ drawableRuleset.Playfield.DisplayJudgements.Value = false;
+
+ bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer();
+
+ drawableRuleset.Overlays.Add(bubbleContainer);
+ }
+
+ public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
+ {
+ drawableObject.OnNewResult += (drawable, _) =>
+ {
+ if (drawable is not DrawableOsuHitObject drawableOsuHitObject) return;
+
+ switch (drawableOsuHitObject.HitObject)
+ {
+ case Slider:
+ case SpinnerTick:
+ break;
+
+ default:
+ addBubble();
+ break;
+ }
+
+ void addBubble()
+ {
+ BubbleDrawable bubble = bubblePool.Get();
+
+ bubble.DrawableOsuHitObject = drawableOsuHitObject;
+ bubble.InitialSize = new Vector2(bubbleSize);
+ bubble.FadeTime = bubbleFade;
+ bubble.MaxSize = maxSize;
+
+ bubbleContainer.Add(bubble);
+ }
+ };
+
+ drawableObject.OnRevertResult += (drawable, _) =>
+ {
+ if (drawable.HitObject is SpinnerTick or Slider) return;
+
+ BubbleDrawable? lastBubble = bubbleContainer.OfType().LastOrDefault();
+
+ lastBubble?.ClearTransforms();
+ lastBubble?.Expire(true);
+ };
+ }
+
+ #region Pooled Bubble drawable
+
+ private partial class BubbleDrawable : PoolableDrawable
+ {
+ public DrawableOsuHitObject? DrawableOsuHitObject { get; set; }
+
+ public Vector2 InitialSize { get; set; }
+
+ public float MaxSize { get; set; }
+
+ public double FadeTime { get; set; }
+
+ private readonly Box colourBox;
+ private readonly CircularContainer content;
+
+ public BubbleDrawable()
+ {
+ Origin = Anchor.Centre;
+ InternalChild = content = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ MaskingSmoothness = 2,
+ BorderThickness = 0,
+ BorderColour = Colour4.White,
+ Masking = true,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Radius = 3,
+ Colour = Colour4.Black.Opacity(0.05f),
+ },
+ Child = colourBox = new Box { RelativeSizeAxes = Axes.Both, }
+ };
+ }
+
+ protected override void PrepareForUse()
+ {
+ Debug.Assert(DrawableOsuHitObject.IsNotNull());
+
+ Colour = DrawableOsuHitObject.IsHit ? Colour4.White : Colour4.Black;
+ Scale = new Vector2(1);
+ Position = getPosition(DrawableOsuHitObject);
+ Size = InitialSize;
+
+ //We want to fade to a darker colour to avoid colours such as white hiding the "ripple" effect.
+ ColourInfo colourDarker = DrawableOsuHitObject.AccentColour.Value.Darken(0.1f);
+
+ // The absolute length of the bubble's animation, can be used in fractions for animations of partial length
+ double duration = 1700 + Math.Pow(FadeTime, 1.07f);
+
+ // Main bubble scaling based on combo
+ this.FadeTo(1)
+ .ScaleTo(MaxSize, duration * 0.8f)
+ .Then()
+ // Pop at the end of the bubbles life time
+ .ScaleTo(MaxSize * 1.5f, duration * 0.2f, Easing.OutQuint)
+ .FadeOut(duration * 0.2f, Easing.OutCirc).Expire();
+
+ if (!DrawableOsuHitObject.IsHit) return;
+
+ content.BorderThickness = InitialSize.X / 3.5f;
+ content.BorderColour = Colour4.White;
+
+ colourBox.FadeColour(colourDarker);
+
+ content.TransformTo(nameof(BorderColour), colourDarker, duration * 0.3f, Easing.OutQuint);
+ // Ripple effect utilises the border to reduce drawable count
+ content.TransformTo(nameof(BorderThickness), 2f, duration * 0.3f, Easing.OutQuint)
+ .Then()
+ // Avoids transparency overlap issues during the bubble "pop"
+ .TransformTo(nameof(BorderThickness), 0f);
+ }
+
+ private Vector2 getPosition(DrawableOsuHitObject drawableObject)
+ {
+ switch (drawableObject)
+ {
+ // SliderHeads are derived from HitCircles,
+ // so we must handle them before to avoid them using the wrong positioning logic
+ case DrawableSliderHead:
+ return drawableObject.HitObject.Position;
+
+ // Using hitobject position will cause issues with HitCircle placement due to stack leniency.
+ case DrawableHitCircle:
+ return drawableObject.Position;
+
+ default:
+ return drawableObject.HitObject.Position;
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs
index 371dfe6a1a..1de6b9ce55 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
index 700a3f44bc..5569df8d95 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs
index 4769e7660b..bf65a6c9d3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
index 38d90eb121..c8c4cd6a14 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!";
public override double ScoreMultiplier => 0.5;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) };
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
index b7838ebaa7..661cc948c5 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNightcore : ModNightcore
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
index 31a6b69d6b..28d459cedb 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Hit objects run away!";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles) };
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index a7b02596d5..664a8146e7 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -133,14 +133,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
- if (HitObject.SampleControlPoint == null)
- {
- throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
- + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
- }
-
- Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray();
- slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray();
+ Samples.Samples = HitObject.TailSamples.Cast().ToArray();
+ slidingSample.Samples = HitObject.CreateSlidingSamples().Cast().ToArray();
}
public override void StopAllSamples()
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index a5193f1b6e..0ceda1d4b0 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.LoadSamples();
- spinningSample.Samples = HitObject.CreateSpinningSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray();
+ spinningSample.Samples = HitObject.CreateSpinningSamples().Cast().ToArray();
spinningSample.Frequency.Value = spinning_sample_initial_frequency;
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 6c2be8a49a..4189f8ba1e 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -10,18 +10,18 @@ using osu.Game.Rulesets.Objects;
using System.Linq;
using System.Threading;
using Newtonsoft.Json;
+using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
- public class Slider : OsuHitObject, IHasPathWithRepeats
+ public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks
{
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
@@ -134,6 +134,21 @@ namespace osu.Game.Rulesets.Osu.Objects
///
public bool OnlyJudgeNestedObjects = true;
+ public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1)
+ {
+ Precision = 0.01,
+ MinValue = 0.1,
+ MaxValue = 10
+ };
+
+ public double SliderVelocity
+ {
+ get => SliderVelocityBindable.Value;
+ set => SliderVelocityBindable.Value = value;
+ }
+
+ public bool GenerateTicks { get; set; } = true;
+
[JsonIgnore]
public SliderHeadCircle HeadCircle { get; protected set; }
@@ -151,15 +166,11 @@ namespace osu.Game.Rulesets.Osu.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
-#pragma warning disable 618
- var legacyDifficultyPoint = DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint;
-#pragma warning restore 618
- double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
- bool generateTicks = legacyDifficultyPoint?.GenerateTicks ?? true;
+ double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
- TickDistance = generateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
+ TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index ed6f8a9a6a..df5898fd67 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Objects
AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
- : new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration });
+ : new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { GetSampleInfo("spinnerbonus") } });
}
}
@@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects
return new[]
{
- SampleControlPoint.ApplyTo(referenceSample).With("spinnerspin")
+ referenceSample.With("spinnerspin")
};
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
index 81cdf5755b..00ceccaf7b 100644
--- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
@@ -3,7 +3,6 @@
#nullable disable
-using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
@@ -11,11 +10,6 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerBonusTick : SpinnerTick
{
- public SpinnerBonusTick()
- {
- Samples.Add(new HitSampleInfo("spinnerbonus"));
- }
-
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 48056a49de..922594a93a 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -203,7 +203,8 @@ namespace osu.Game.Rulesets.Osu
new OsuModNoScope(),
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed(),
- new OsuModFreezeFrame()
+ new OsuModFreezeFrame(),
+ new OsuModBubbles()
};
case ModType.System:
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index 8fdf3821fa..52fdfea95f 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu
Cursor,
CursorTrail,
CursorParticles,
+ CursorRipple,
SliderScorePoint,
ReverseArrow,
HitCircleText,
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
index d6ce793c7e..461b4a3b45 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
@@ -98,7 +98,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
- this.FadeOut(duration, Easing.OutQuint);
+ // intentionally pile on an extra FadeOut to make it happen much faster
+ this.FadeOut(duration / 4, Easing.OutQuint);
icon.ScaleTo(defaultIconScale * icon_scale, duration, Easing.OutQuint);
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs
index fbe094ef81..e6166e9441 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs
@@ -3,11 +3,13 @@
using System.Diagnostics;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
@@ -18,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private Drawable proxy = null!;
+ private Bindable accentColour = null!;
+
+ private bool textureIsDefaultSkin;
+
+ private Drawable arrow = null!;
+
[BackgroundDependencyLoader]
private void load(ISkinSource skinSource)
{
@@ -26,7 +34,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
string lookupName = new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow).LookupName;
var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
- InternalChild = skin?.GetAnimation(lookupName, true, true) ?? Empty();
+
+ InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true) ?? Empty());
+ textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
}
protected override void LoadComplete()
@@ -39,6 +49,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
drawableHitObject.HitObjectApplied += onHitObjectApplied;
onHitObjectApplied(drawableHitObject);
+
+ accentColour = drawableHitObject.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(c =>
+ {
+ arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White;
+ }, true);
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index 620540b8ef..f049aa088f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -100,6 +100,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
+ case OsuSkinComponents.CursorRipple:
+ if (GetTexture("cursor-ripple") != null)
+ {
+ var ripple = this.GetAnimation("cursor-ripple", false, false);
+
+ // In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
+ // If anyone complains about these not being applied, this can be uncommented.
+ //
+ // But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
+ // so we might be okay.
+ //
+ // if (ripple != null)
+ // {
+ // ripple.Scale = new Vector2(0.5f);
+ // ripple.Alpha = 0.2f;
+ // }
+
+ return ripple;
+ }
+
+ return null;
+
case OsuSkinComponents.CursorParticles:
if (GetTexture("star2") != null)
return new LegacyCursorParticles();
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs
new file mode 100644
index 0000000000..076d97d06a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs
@@ -0,0 +1,105 @@
+// 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.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Osu.Configuration;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.UI.Cursor
+{
+ public partial class CursorRippleVisualiser : CompositeDrawable, IKeyBindingHandler
+ {
+ private readonly Bindable showRipples = new Bindable(true);
+
+ private readonly DrawablePool ripplePool = new DrawablePool(20);
+
+ public CursorRippleVisualiser()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ public Vector2 CursorScale { get; set; } = Vector2.One;
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuRulesetConfigManager? rulesetConfig)
+ {
+ rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples);
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (showRipples.Value)
+ {
+ AddInternal(ripplePool.Get(r =>
+ {
+ r.Position = e.MousePosition;
+ r.Scale = CursorScale;
+ }));
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+
+ private partial class CursorRipple : PoolableDrawable
+ {
+ private Drawable ripple = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+ Origin = Anchor.Centre;
+
+ InternalChild = ripple = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorRipple), _ => new DefaultCursorRipple())
+ {
+ Blending = BlendingParameters.Additive,
+ };
+ }
+
+ protected override void PrepareForUse()
+ {
+ base.PrepareForUse();
+
+ ClearTransforms(true);
+
+ ripple.ScaleTo(0.1f)
+ .ScaleTo(1, 700, Easing.Out);
+
+ this
+ .FadeOutFromOne(700)
+ .Expire(true);
+ }
+ }
+
+ public partial class DefaultCursorRipple : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new RingPiece(3)
+ {
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2),
+ Alpha = 0.1f,
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
index 5d7648b073..bf1ff872dd 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
@@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Bindable userCursorScale;
private Bindable autoCursorScale;
+ private readonly CursorRippleVisualiser rippleVisualiser;
+
public OsuCursorContainer()
{
InternalChild = fadeContainer = new Container
@@ -48,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Children = new[]
{
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
+ rippleVisualiser = new CursorRippleVisualiser(),
new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling),
}
};
@@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
var newScale = new Vector2(e.NewValue);
ActiveCursor.Scale = newScale;
+ rippleVisualiser.CursorScale = newScale;
cursorTrail.Scale = newScale;
}, true);
diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
index 64c4e7eef6..0e410dbf57 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
@@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI
LabelText = RulesetSettingsStrings.CursorTrail,
Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail)
},
+ new SettingsCheckbox
+ {
+ LabelText = RulesetSettingsStrings.CursorRipples,
+ Current = config.GetBindable(OsuRulesetSetting.ShowCursorRipples)
+ },
new SettingsEnumDropdown
{
LabelText = RulesetSettingsStrings.PlayfieldBorderStyle,
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 6a35e9376b..e298e313df 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -64,7 +64,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
foreach (HitObject hitObject in original.HitObjects)
{
- double nextScrollSpeed = hitObject.DifficultyControlPoint.SliderVelocity;
+ if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
+
+ double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
@@ -131,7 +133,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = taikoDuration,
- TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4
+ TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4,
+ SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
};
}
@@ -177,15 +180,14 @@ 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 = obj.DifficultyControlPoint;
double beatLength;
-#pragma warning disable 618
- if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
-#pragma warning restore 618
- beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
+ if (obj.LegacyBpmMultiplier.HasValue)
+ beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
+ else if (obj is IHasSliderVelocity hasSliderVelocity)
+ beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
else
- beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
+ beatLength = timingPoint.BeatLength;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
index d0361b1c8d..cdeaafde10 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
- drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
+ drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs
index 84aa5e6bba..f442435d9c 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
index 89581c57bd..e517439ba4 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs
index 68d6305fbf..9ef6fe8649 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
index 7cb14635ff..ad5da3d601 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModNightcore : ModNightcore
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index 3325eda7cf..b4a12fd314 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -3,8 +3,11 @@
#nullable disable
+using System.Linq;
using osu.Game.Rulesets.Objects.Types;
using System.Threading;
+using osu.Framework.Bindables;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
@@ -15,7 +18,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects
{
- public class DrumRoll : TaikoStrongableHitObject, IHasPath
+ public class DrumRoll : TaikoStrongableHitObject, IHasPath, IHasSliderVelocity
{
///
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@@ -35,6 +38,19 @@ namespace osu.Game.Rulesets.Taiko.Objects
///
public double Velocity { get; private set; }
+ public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1)
+ {
+ Precision = 0.01,
+ MinValue = 0.1,
+ MaxValue = 10
+ };
+
+ public double SliderVelocity
+ {
+ get => SliderVelocityBindable.Value;
+ set => SliderVelocityBindable.Value = value;
+ }
+
///
/// Numer of ticks per beat length.
///
@@ -52,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
+ double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
tickSpacing = timingPoint.BeatLength / TickRate;
@@ -81,7 +97,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
FirstTick = first,
TickSpacing = tickSpacing,
StartTime = t,
- IsStrong = IsStrong
+ IsStrong = IsStrong,
+ Samples = Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToList()
});
first = false;
diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
index d4d59d5d44..0043f231d2 100644
--- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
if (IsStrongBindable.Value != strongSamples.Any())
{
if (IsStrongBindable.Value)
- Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
+ Samples.Add(GetSampleInfo(HitSampleInfo.HIT_FINISH));
else
{
foreach (var sample in strongSamples)
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index a08877e2dd..64d406a308 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public new BindableDouble TimeRange => base.TimeRange;
- public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
+ public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true);
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
@@ -69,7 +69,9 @@ namespace osu.Game.Rulesets.Taiko.UI
const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
- float ratio = DrawHeight / 480;
+ // Width is used because it defines how many notes fit on the playfield.
+ // We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
+ float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
}
@@ -92,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
- LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect }
+ LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange }
};
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs
index 4809791af8..92f2b74568 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
@@ -17,12 +18,12 @@ namespace osu.Game.Rulesets.Taiko.UI
public void Play(HitType hitType)
{
- var hitObject = GetMostValidObject();
+ var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
- if (hitObject == null)
+ if (hitSample == null)
return;
- PlaySamples(new ISampleInfo[] { hitObject.SampleControlPoint.GetSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) });
+ PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) });
}
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
index 42732d90e4..3587783104 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
@@ -11,9 +11,11 @@ namespace osu.Game.Rulesets.Taiko.UI
public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
- private const float default_aspect = 16f / 9f;
- public readonly IBindable LockPlayfieldMaxAspect = new BindableBool(true);
+ public const float MAXIMUM_ASPECT = 16f / 9f;
+ public const float MINIMUM_ASPECT = 5f / 4f;
+
+ public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true);
protected override void Update()
{
@@ -26,12 +28,22 @@ namespace osu.Game.Rulesets.Taiko.UI
//
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
// This is still a bit weird, because readability changes with window size, but it is what it is.
- if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect)
- height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
+ if (LockPlayfieldAspectRange.Value)
+ {
+ float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y;
+ if (currentAspect > MAXIMUM_ASPECT)
+ height *= currentAspect / MAXIMUM_ASPECT;
+ else if (currentAspect < MINIMUM_ASPECT)
+ height *= currentAspect / MINIMUM_ASPECT;
+ }
+
+ // Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
+ height = Math.Min(height, 1f / 3f);
Height = height;
- // Position the taiko playfield exactly one playfield from the top of the screen.
+ // Position the taiko playfield exactly one playfield from the top of the screen, if there is enough space for it.
+ // Note that the relative height cannot exceed one-third - if that limit is hit, the playfield will be exactly centered.
RelativePositionAxes = Axes.Y;
Y = height;
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 518981980b..2c4f193327 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -160,6 +160,36 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeVideoWithLowercaseExtension()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var beatmap = decoder.Decode(stream);
+ var metadata = beatmap.Metadata;
+
+ Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
+ }
+ }
+
+ [Test]
+ public void TestDecodeVideoWithUppercaseExtension()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var beatmap = decoder.Decode(stream);
+ var metadata = beatmap.Metadata;
+
+ Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
+ }
+ }
+
[Test]
public void TestDecodeImageSpecifiedAsVideo()
{
@@ -480,7 +510,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
}
- static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]);
+ static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
}
[Test]
@@ -498,7 +528,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Gameplay/normal-hitnormal3", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
}
- static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]);
+ static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
}
[Test]
@@ -518,7 +548,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume);
}
- static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]);
+ static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
}
[Test]
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
index 3a776ac225..34ff8bfd84 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
@@ -95,6 +95,27 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestLoopWithoutExplicitFadeOut()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("animation-loop-no-explicit-end-time.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
+ Assert.AreEqual(1, background.Elements.Count);
+
+ Assert.AreEqual(2000, background.Elements[0].StartTime);
+ Assert.AreEqual(2000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime);
+
+ Assert.AreEqual(3000, (background.Elements[0] as StoryboardAnimation)?.GetEndTime());
+ Assert.AreEqual(12000, (background.Elements[0] as StoryboardAnimation)?.EndTimeForDisplay);
+ }
+ }
+
[Test]
public void TestCorrectAnimationStartTime()
{
@@ -169,6 +190,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeVideoWithLowercaseExtension()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
+ Assert.That(video.Elements.Count, Is.EqualTo(1));
+
+ Assert.AreEqual("Video.avi", ((StoryboardVideo)video.Elements[0]).Path);
+ }
+ }
+
+ [Test]
+ public void TestDecodeVideoWithUppercaseExtension()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
+ Assert.That(video.Elements.Count, Is.EqualTo(1));
+
+ Assert.AreEqual("Video.AVI", ((StoryboardVideo)video.Elements[0]).Path);
+ }
+ }
+
[Test]
public void TestDecodeImageSpecifiedAsVideo()
{
@@ -179,8 +234,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
var storyboard = decoder.Decode(stream);
- StoryboardLayer foreground = storyboard.Layers.Single(l => l.Name == "Video");
- Assert.That(foreground.Elements.Count, Is.Zero);
+ StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
+ Assert.That(video.Elements.Count, Is.Zero);
}
}
diff --git a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
index 1e1c214c30..5a3ef619d1 100644
--- a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
+++ b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
@@ -37,45 +37,6 @@ namespace osu.Game.Tests.Editing.Checks
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
}
- [Test]
- public void TestNormalControlPointVolume()
- {
- var hitCircle = new HitCircle
- {
- StartTime = 0,
- Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
- };
- hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
-
- assertOk(new List { hitCircle });
- }
-
- [Test]
- public void TestLowControlPointVolume()
- {
- var hitCircle = new HitCircle
- {
- StartTime = 1000,
- Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
- };
- hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
-
- assertLowVolume(new List { hitCircle });
- }
-
- [Test]
- public void TestMutedControlPointVolume()
- {
- var hitCircle = new HitCircle
- {
- StartTime = 2000,
- Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
- };
- hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
-
- assertMuted(new List { hitCircle });
- }
-
[Test]
public void TestNormalSampleVolume()
{
@@ -122,7 +83,7 @@ namespace osu.Game.Tests.Editing.Checks
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
- Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
@@ -135,7 +96,7 @@ namespace osu.Game.Tests.Editing.Checks
var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{
- Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
@@ -155,13 +116,13 @@ namespace osu.Game.Tests.Editing.Checks
var sliderTick = new SliderTick
{
StartTime = 250,
- Samples = new List { new HitSampleInfo("slidertick") }
+ Samples = new List { new HitSampleInfo("slidertick", volume: volume_regular) }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{
- Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail.
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } // Applies to the tail.
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
@@ -174,14 +135,14 @@ namespace osu.Game.Tests.Editing.Checks
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
- Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
- Samples = new List { new HitSampleInfo("slidertick") }
+ Samples = new List { new HitSampleInfo("slidertick", volume: volume_regular) }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
@@ -194,59 +155,6 @@ namespace osu.Game.Tests.Editing.Checks
assertMutedPassive(new List { slider });
}
- [Test]
- public void TestMutedControlPointVolumeSliderHead()
- {
- var sliderHead = new SliderHeadCircle
- {
- StartTime = 2000,
- Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
- };
- sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
-
- var sliderTick = new SliderTick
- {
- StartTime = 2250,
- Samples = new List { new HitSampleInfo("slidertick") }
- };
- sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
-
- var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
- {
- Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
- };
- slider.ApplyDefaults(cpi, new BeatmapDifficulty());
-
- assertMuted(new List { slider });
- }
-
- [Test]
- public void TestMutedControlPointVolumeSliderTail()
- {
- var sliderHead = new SliderHeadCircle
- {
- StartTime = 0,
- Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
- };
- sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
-
- var sliderTick = new SliderTick
- {
- StartTime = 250,
- Samples = new List { new HitSampleInfo("slidertick") }
- };
- sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
-
- // Ends after the 5% control point.
- var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
- {
- Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
- };
- slider.ApplyDefaults(cpi, new BeatmapDifficulty());
-
- assertMutedPassive(new List { slider });
- }
-
private void assertOk(List hitObjects)
{
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index f556f6e2fe..6399507aa0 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
@@ -74,12 +75,9 @@ namespace osu.Game.Tests.Editing
[TestCase(2)]
public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
{
- assertSnapDistance(100, new HitObject
+ assertSnapDistance(100, new Slider
{
- DifficultyControlPoint = new DifficultyControlPoint
- {
- SliderVelocity = multiplier
- }
+ SliderVelocity = multiplier
}, false);
}
@@ -87,12 +85,9 @@ namespace osu.Game.Tests.Editing
[TestCase(2)]
public void TestSpeedMultiplierDoesChangeDistanceSnap(float multiplier)
{
- assertSnapDistance(100 * multiplier, new HitObject
+ assertSnapDistance(100 * multiplier, new Slider
{
- DifficultyControlPoint = new DifficultyControlPoint
- {
- SliderVelocity = multiplier
- }
+ SliderVelocity = multiplier
}, true);
}
@@ -114,12 +109,9 @@ namespace osu.Game.Tests.Editing
const float base_distance = 100;
const float slider_velocity = 1.2f;
- var referenceObject = new HitObject
+ var referenceObject = new Slider
{
- DifficultyControlPoint = new DifficultyControlPoint
- {
- SliderVelocity = slider_velocity
- }
+ SliderVelocity = slider_velocity
};
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
diff --git a/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs b/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs
index c70ad751be..99b85d0502 100644
--- a/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs
+++ b/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs
@@ -1,11 +1,14 @@
-#include "sh_Utils.h"
+#define HIGH_PRECISION_VERTEX
-varying mediump vec2 v_TexCoord;
-varying mediump vec4 v_TexRect;
+#include "sh_Utils.h"
+#include "sh_Masking.h"
+
+layout(location = 2) in highp vec2 v_TexCoord;
+
+layout(location = 0) out vec4 o_Colour;
void main(void)
{
- float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]);
- gl_FragColor = hsv2rgb(vec4(hueValue, 1, 1, 1));
+ highp float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]);
+ o_Colour = getRoundedColor(hsv2rgb(vec4(hueValue, 1, 1, 1)), v_TexCoord);
}
-
diff --git a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs
index 4485356fa4..505554bb33 100644
--- a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs
+++ b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs
@@ -1,31 +1,25 @@
-#include "sh_Utils.h"
+layout(location = 0) in highp vec2 m_Position;
+layout(location = 1) in lowp vec4 m_Colour;
+layout(location = 2) in highp vec2 m_TexCoord;
+layout(location = 3) in highp vec4 m_TexRect;
+layout(location = 4) in mediump vec2 m_BlendRange;
-attribute highp vec2 m_Position;
-attribute lowp vec4 m_Colour;
-attribute mediump vec2 m_TexCoord;
-attribute mediump vec4 m_TexRect;
-attribute mediump vec2 m_BlendRange;
-
-varying highp vec2 v_MaskingPosition;
-varying lowp vec4 v_Colour;
-varying mediump vec2 v_TexCoord;
-varying mediump vec4 v_TexRect;
-varying mediump vec2 v_BlendRange;
-
-uniform highp mat4 g_ProjMatrix;
-uniform highp mat3 g_ToMaskingSpace;
+layout(location = 0) out highp vec2 v_MaskingPosition;
+layout(location = 1) out lowp vec4 v_Colour;
+layout(location = 2) out highp vec2 v_TexCoord;
+layout(location = 3) out highp vec4 v_TexRect;
+layout(location = 4) out mediump vec2 v_BlendRange;
void main(void)
{
- // Transform from screen space to masking space.
- highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
- v_MaskingPosition = maskingPos.xy / maskingPos.z;
+ // Transform from screen space to masking space.
+ highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
+ v_MaskingPosition = maskingPos.xy / maskingPos.z;
- v_Colour = m_Colour;
- v_TexCoord = m_TexCoord;
- v_TexRect = m_TexRect;
- v_BlendRange = m_BlendRange;
+ v_Colour = m_Colour;
+ v_TexCoord = m_TexCoord;
+ v_TexRect = m_TexRect;
+ v_BlendRange = m_BlendRange;
- gl_Position = gProjMatrix * vec4(m_Position, 1.0, 1.0);
+ gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0);
}
-
diff --git a/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb b/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb
new file mode 100644
index 0000000000..7afaa445df
--- /dev/null
+++ b/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb
@@ -0,0 +1,6 @@
+[Events]
+//Storyboard Layer 0 (Background)
+Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever
+ F,0,2000,,0,1
+ L,2000,10
+ F,18,0,1000,1,0
diff --git a/osu.Game.Tests/Resources/video-with-lowercase-extension.osb b/osu.Game.Tests/Resources/video-with-lowercase-extension.osb
new file mode 100644
index 0000000000..eec09722ed
--- /dev/null
+++ b/osu.Game.Tests/Resources/video-with-lowercase-extension.osb
@@ -0,0 +1,5 @@
+osu file format v14
+
+[Events]
+0,0,"BG.jpg",0,0
+Video,0,"Video.avi",0,0
diff --git a/osu.Game.Tests/Resources/video-with-uppercase-extension.osb b/osu.Game.Tests/Resources/video-with-uppercase-extension.osb
new file mode 100644
index 0000000000..3834a547f2
--- /dev/null
+++ b/osu.Game.Tests/Resources/video-with-uppercase-extension.osb
@@ -0,0 +1,5 @@
+osu file format v14
+
+[Events]
+0,0,"BG.jpg",0,0
+Video,0,"Video.AVI",0,0
diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
index f8248e88bb..6639b6dd68 100644
--- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
@@ -164,7 +164,7 @@ namespace osu.Game.Tests.Rulesets
this.parentManager = parentManager;
}
- public override byte[] LoadRaw(string name) => parentManager.LoadRaw(name);
+ public override byte[] GetRawData(string fileName) => parentManager.GetRawData(fileName);
public bool IsDisposed { get; private set; }
diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
index a5a83d7231..f0a9ce7beb 100644
--- a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
+++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
@@ -51,8 +51,10 @@ namespace osu.Game.Tests.Testing
{
AddStep("ruleset shaders retrieved without error", () =>
{
- Dependencies.Get().LoadRaw(@"sh_TestVertex.vs");
- Dependencies.Get().LoadRaw(@"sh_TestFragment.fs");
+ Dependencies.Get().GetRawData(@"sh_TestVertex.vs");
+ Dependencies.Get().GetRawData(@"sh_TestFragment.fs");
+ Dependencies.Get().Load(@"TestVertex", @"TestFragment");
+ Dependencies.Get().Load(VertexShaderDescriptor.TEXTURE_2, @"TestFragment");
});
}
diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
index fbdaad1cd8..8f4250799e 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
@@ -311,6 +311,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsDrawable => true;
public double StartTime => double.MinValue;
public double EndTime => double.MaxValue;
+ public double EndTimeForDisplay => double.MaxValue;
public Drawable CreateDrawable() => new DrawableTestStoryboardElement();
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs
index d26bb6bb8a..c4c05278b5 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs
@@ -6,7 +6,6 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
-using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
@@ -95,10 +94,6 @@ namespace osu.Game.Tests.Visual.Editing
var path = slider.Path;
return path.ControlPoints.Count == 2 && path.ControlPoints.SequenceEqual(addedObject.Path.ControlPoints);
});
-
- // see `HitObject.control_point_leniency`.
- AddAssert("sample control point has correct time", () => Precision.AlmostEquals(slider.SampleControlPoint.Time, slider.GetEndTime(), 1));
- AddAssert("difficulty control point has correct time", () => slider.DifficultyControlPoint.Time == slider.StartTime);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
index b396b382ff..64c48e74cf 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -122,19 +122,9 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Beatmap has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
- // After placement these must be non-default as defaults are read-only.
- AddAssert("Placed object has non-default control points", () =>
- !ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) &&
- !ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
-
ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
-
- // After placement these must be non-default as defaults are read-only.
- AddAssert("Placed object still has non-default control points", () =>
- !ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) &&
- !ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
}
[Test]
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
index 7ab0188114..9bdb9a513c 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
@@ -108,12 +108,16 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType().First(d => d.Button.Label == "HitCircle").TriggerClick());
+ ExpandingToolboxContainer toolboxContainer = null!;
+
+ AddStep("move mouse to toolbox", () => InputManager.MoveMouseTo(toolboxContainer = hitObjectComposer.ChildrenOfType().First()));
+ AddUntilStep("toolbox is expanded", () => toolboxContainer.Expanded.Value);
+ AddUntilStep("wait for toolbox to expand", () => toolboxContainer.LatestTransformEndTime, () => Is.EqualTo(Time.Current));
+
AddStep("move mouse to overlapping toggle button", () =>
{
var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad;
- var button = hitObjectComposer
- .ChildrenOfType().First()
- .ChildrenOfType().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre));
+ var button = toolboxContainer.ChildrenOfType().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre));
InputManager.MoveMouseTo(button);
});
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs
index ab82678eb9..3b998b4219 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs
@@ -8,10 +8,10 @@ using Humanizer;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
@@ -61,10 +61,7 @@ namespace osu.Game.Tests.Visual.Editing
new PathControlPoint(new Vector2(100, 0))
}
},
- DifficultyControlPoint = new DifficultyControlPoint
- {
- SliderVelocity = 2
- }
+ SliderVelocity = 2
});
});
}
@@ -100,8 +97,8 @@ namespace osu.Game.Tests.Visual.Editing
{
AddStep("unify slider velocity", () =>
{
- foreach (var h in EditorBeatmap.HitObjects)
- h.DifficultyControlPoint.SliderVelocity = 1.5;
+ foreach (var h in EditorBeatmap.HitObjects.OfType())
+ h.SliderVelocity = 1.5;
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
@@ -185,7 +182,7 @@ namespace osu.Game.Tests.Visual.Editing
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
- return h.DifficultyControlPoint.SliderVelocity == velocity;
+ return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocity == velocity;
});
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs
index e8dcc6f19b..7a0418cfec 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs
@@ -4,11 +4,12 @@
#nullable disable
using System.Linq;
+using System.Collections.Generic;
using Humanizer;
using NUnit.Framework;
using osu.Framework.Testing;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets;
@@ -39,10 +40,9 @@ namespace osu.Game.Tests.Visual.Editing
{
StartTime = 0,
Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2,
- SampleControlPoint = new SampleControlPoint
+ Samples = new List
{
- SampleBank = "normal",
- SampleVolume = 80
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 80)
}
});
@@ -50,10 +50,9 @@ namespace osu.Game.Tests.Visual.Editing
{
StartTime = 500,
Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2,
- SampleControlPoint = new SampleControlPoint
+ Samples = new List
{
- SampleBank = "soft",
- SampleVolume = 60
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft", volume: 60)
}
});
});
@@ -96,7 +95,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("unify sample volume", () =>
{
foreach (var h in EditorBeatmap.HitObjects)
- h.SampleControlPoint.SampleVolume = 50;
+ {
+ for (int i = 0; i < h.Samples.Count; i++)
+ {
+ h.Samples[i] = h.Samples[i].With(newVolume: 50);
+ }
+ }
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
@@ -136,7 +140,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("unify sample bank", () =>
{
foreach (var h in EditorBeatmap.HitObjects)
- h.SampleControlPoint.SampleBank = "soft";
+ {
+ for (int i = 0; i < h.Samples.Count; i++)
+ {
+ h.Samples[i] = h.Samples[i].With(newBank: "soft");
+ }
+ }
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
@@ -248,7 +257,7 @@ namespace osu.Game.Tests.Visual.Editing
private void hitObjectHasSampleVolume(int objectIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} has volume {volume}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
- return h.SampleControlPoint.SampleVolume == volume;
+ return h.Samples.All(o => o.Volume == volume);
});
private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
@@ -265,7 +274,7 @@ namespace osu.Game.Tests.Visual.Editing
private void hitObjectHasSampleBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has bank {bank}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
- return h.SampleControlPoint.SampleBank == bank;
+ return h.Samples.All(o => o.Bank == bank);
});
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs
new file mode 100644
index 0000000000..7f9a69833c
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Extensions;
+using osu.Game.Database;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public partial class TestSceneLocallyModifyingOnlineBeatmaps : EditorSavingTestScene
+ {
+ public override void SetUpSteps()
+ {
+ CreateInitialBeatmap = () =>
+ {
+ var importedSet = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).GetResultSafely();
+ return Game.BeatmapManager.GetWorkingBeatmap(importedSet!.Value.Beatmaps.First());
+ };
+
+ base.SetUpSteps();
+ }
+
+ [Test]
+ public void TestLocallyModifyingOnlineBeatmap()
+ {
+ AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0));
+
+ AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0));
+ SaveEditor();
+
+ ReloadEditorToSameBeatmap();
+ AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
index 709d796e97..08e036248b 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
@@ -77,5 +77,39 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("object has non-zero duration", () => EditorBeatmap.HitObjects.OfType().Single().Duration > 0);
}
+
+ [Test]
+ public void TestDisallowRepeatsOnZeroDurationObjects()
+ {
+ DragArea dragArea;
+
+ AddStep("add zero length slider", () =>
+ {
+ EditorBeatmap.Clear();
+ EditorBeatmap.Add(new Slider
+ {
+ Position = new Vector2(256, 256),
+ StartTime = 2700
+ });
+ });
+
+ AddStep("hold down drag bar", () =>
+ {
+ // distinguishes between the actual drag bar and its "underlay shadow".
+ dragArea = this.ChildrenOfType().Single(bar => bar.HandlePositionalInput);
+ InputManager.MoveMouseTo(dragArea);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("try to extend drag bar", () =>
+ {
+ var blueprint = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(blueprint.SelectionQuad.TopLeft + new Vector2(100, 0));
+ });
+
+ AddStep("release button", () => InputManager.PressButton(MouseButton.Left));
+
+ AddAssert("object has zero repeats", () => EditorBeatmap.HitObjects.OfType().Single().RepeatCount == 0);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
index 31133f00d9..114c554d28 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
@@ -73,8 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay
new HitCircle
{
StartTime = t += spacing,
- Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
- SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
+ Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") },
},
new HitCircle
{
@@ -84,8 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
StartTime = t += spacing,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
- Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) },
- SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
+ Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, "soft") },
},
});
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index eecead5415..ae46dda750 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter;
- private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single();
+ private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single();
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
index 46d5e6c4d2..22f7111f68 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
@@ -7,7 +7,9 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Utils;
+using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
+using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -17,13 +19,21 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public TestSceneKeyCounter()
{
- KeyCounterDisplay kc = new DefaultKeyCounterDisplay
+ KeyCounterDisplay defaultDisplay = new DefaultKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
+ Position = new Vector2(0, 72.7f)
};
- kc.AddRange(new InputTrigger[]
+ KeyCounterDisplay argonDisplay = new ArgonKeyCounterDisplay
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Position = new Vector2(0, -72.7f)
+ };
+
+ defaultDisplay.AddRange(new InputTrigger[]
{
new KeyCounterKeyboardTrigger(Key.X),
new KeyCounterKeyboardTrigger(Key.X),
@@ -31,30 +41,41 @@ namespace osu.Game.Tests.Visual.Gameplay
new KeyCounterMouseTrigger(MouseButton.Right),
});
- var testCounter = (DefaultKeyCounter)kc.Counters.First();
+ argonDisplay.AddRange(new InputTrigger[]
+ {
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterMouseTrigger(MouseButton.Left),
+ new KeyCounterMouseTrigger(MouseButton.Right),
+ });
+
+ var testCounter = (DefaultKeyCounter)defaultDisplay.Counters.First();
AddStep("Add random", () =>
{
Key key = (Key)((int)Key.A + RNG.Next(26));
- kc.Add(new KeyCounterKeyboardTrigger(key));
+ defaultDisplay.Add(new KeyCounterKeyboardTrigger(key));
+ argonDisplay.Add(new KeyCounterKeyboardTrigger(key));
});
- Key testKey = ((KeyCounterKeyboardTrigger)kc.Counters.First().Trigger).Key;
-
- void addPressKeyStep()
- {
- AddStep($"Press {testKey} key", () => InputManager.Key(testKey));
- }
+ Key testKey = ((KeyCounterKeyboardTrigger)defaultDisplay.Counters.First().Trigger).Key;
addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1);
addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2);
- AddStep("Disable counting", () => testCounter.IsCounting.Value = false);
+ AddStep("Disable counting", () =>
+ {
+ argonDisplay.IsCounting.Value = false;
+ defaultDisplay.IsCounting.Value = false;
+ });
addPressKeyStep();
AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2);
- Add(kc);
+ Add(defaultDisplay);
+ Add(argonDisplay);
+
+ void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey));
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
index 0469df1de3..d16f51f36e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
@@ -164,6 +164,36 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("all results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0));
}
+ [Test]
+ public void TestRevertNestedObjects()
+ {
+ ManualClock clock = null;
+
+ var beatmap = new Beatmap();
+ beatmap.HitObjects.Add(new TestHitObjectWithNested { Duration = 40 });
+
+ createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
+
+ AddStep("skip to middle of object", () => clock.CurrentTime = (beatmap.HitObjects[0].StartTime + beatmap.HitObjects[0].GetEndTime()) / 2);
+ AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2));
+
+ AddStep("skip to before end of object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() - 1);
+ AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
+
+ DrawableHitObject drawableHitObject = null;
+ HashSet revertedHitObjects = new HashSet();
+
+ AddStep("retrieve drawable hit object", () => drawableHitObject = playfield.ChildrenOfType().Single());
+ AddStep("set up revert tracking", () =>
+ {
+ revertedHitObjects.Clear();
+ drawableHitObject.OnRevertResult += (ho, _) => revertedHitObjects.Add(ho.HitObject);
+ });
+ AddStep("skip back to object start", () => clock.CurrentTime = beatmap.HitObjects[0].StartTime);
+ AddAssert("3 reverts fired", () => revertedHitObjects, () => Has.Count.EqualTo(3));
+ AddAssert("no objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0));
+ }
+
[Test]
public void TestApplyHitResultOnKilled()
{
@@ -258,6 +288,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
RegisterPool(poolSize);
RegisterPool(poolSize);
+ RegisterPool(poolSize);
+ RegisterPool(poolSize);
}
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
@@ -388,6 +420,120 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
+ private class TestHitObjectWithNested : TestHitObject
+ {
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ base.CreateNestedHitObjects(cancellationToken);
+
+ for (int i = 0; i < 3; ++i)
+ AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 });
+ }
+ }
+
+ private class NestedHitObject : ConvertHitObject
+ {
+ }
+
+ private partial class DrawableTestHitObjectWithNested : DrawableHitObject
+ {
+ private Container nestedContainer;
+
+ public DrawableTestHitObjectWithNested()
+ : base(null)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.Red
+ },
+ nestedContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ });
+ }
+
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ Size = new Vector2(200, 50);
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+ nestedContainer.Add(hitObject);
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ nestedContainer.Clear(false);
+ }
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ base.CheckForResult(userTriggered, timeOffset);
+ if (timeOffset >= 0)
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ }
+ }
+
+ private partial class DrawableNestedHitObject : DrawableHitObject
+ {
+ public DrawableNestedHitObject()
+ : this(null)
+ {
+ }
+
+ public DrawableNestedHitObject(NestedHitObject hitObject)
+ : base(hitObject)
+ {
+ Size = new Vector2(15);
+ Colour = Colour4.White;
+ RelativePositionAxes = Axes.Both;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ });
+ }
+
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ X = (float)((HitObject.StartTime - ParentHitObject!.HitObject.StartTime) / (ParentHitObject.HitObject.GetEndTime() - ParentHitObject.HitObject.StartTime));
+ Y = 0.5f;
+
+ LifetimeStart = ParentHitObject.LifetimeStart;
+ LifetimeEnd = ParentHitObject.LifetimeEnd;
+ }
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ base.CheckForResult(userTriggered, timeOffset);
+ if (timeOffset >= 0)
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ }
+ }
+
#endregion
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
index 7bbfc6a62b..0439656aae 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter;
- private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single();
+ private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single();
[Test]
public void TestComboCounterIncrementing()
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index c0b6a0beab..9b130071cc 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -98,6 +98,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) });
AddAssert("freemods empty", () => songSelect.FreeMods.Value.Count == 0);
+
+ // A previous test's mod overlay could still be fading out.
+ AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType().Count() == 1);
+
assertHasFreeModButton(allowedMod, false);
assertHasFreeModButton(requiredMod, false);
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs
new file mode 100644
index 0000000000..603573058e
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Screens;
+using osu.Game.Beatmaps;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Menu;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+ public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
+ {
+ ///
+ /// When entering the editor, a new beatmap is created as part of the asynchronous load process.
+ /// This test ensures that in the case of an early exit from the editor (ie. while it's still loading)
+ /// doesn't leave a dangling beatmap behind.
+ ///
+ /// This may not fail 100% due to timing, but has a pretty high chance of hitting a failure so works well enough
+ /// as a test.
+ ///
+ [Test]
+ public void TestCancelNavigationToEditor()
+ {
+ BeatmapSetInfo[] beatmapSets = null!;
+
+ AddStep("Fetch initial beatmaps", () => beatmapSets = allBeatmapSets());
+
+ AddStep("Set current beatmap to default", () => Game.Beatmap.SetDefault());
+
+ AddStep("Push editor loader", () => Game.ScreenStack.Push(new EditorLoader()));
+ AddUntilStep("Wait for loader current", () => Game.ScreenStack.CurrentScreen is EditorLoader);
+ AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit());
+
+ AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
+ AddAssert("Check no new beatmaps were made", () => allBeatmapSets().SequenceEqual(beatmapSets));
+
+ BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All().Where(x => !x.DeletePending).ToArray());
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 5d13421195..a27c4ddad2 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -14,6 +14,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables;
+using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -26,7 +27,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Tests.Visual.Online
{
- public partial class TestSceneBeatmapSetOverlay : OsuTestScene
+ public partial class TestSceneBeatmapSetOverlay : OsuManualInputManagerTestScene
{
private readonly TestBeatmapSetOverlay overlay;
@@ -281,6 +282,22 @@ namespace osu.Game.Tests.Visual.Online
AddAssert(@"type is correct", () => type == lookupType.ToString());
}
+ [Test]
+ public void TestBeatmapSetWithGuestDifficulty()
+ {
+ AddStep("show map", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty()));
+ AddStep("move mouse to host difficulty", () =>
+ {
+ InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(0));
+ });
+ AddAssert("guest mapper information not shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().All(s => s.Text != "BanchoBot"));
+ AddStep("move mouse to guest difficulty", () =>
+ {
+ InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(1));
+ });
+ AddAssert("guest mapper information shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().Any(s => s.Text == "BanchoBot"));
+ }
+
private APIBeatmapSet createManyDifficultiesBeatmapSet()
{
var set = getBeatmapSet();
@@ -320,6 +337,60 @@ namespace osu.Game.Tests.Visual.Online
return beatmapSet;
}
+ private APIBeatmapSet createBeatmapSetWithGuestDifficulty()
+ {
+ var set = getBeatmapSet();
+
+ var beatmaps = new List();
+
+ var guestUser = new APIUser
+ {
+ Username = @"BanchoBot",
+ Id = 3,
+ };
+
+ set.RelatedUsers = new[]
+ {
+ set.Author, guestUser
+ };
+
+ beatmaps.Add(new APIBeatmap
+ {
+ OnlineID = 1145,
+ DifficultyName = "Host Diff",
+ RulesetID = Ruleset.Value.OnlineID,
+ StarRating = 1.4,
+ OverallDifficulty = 3.5f,
+ AuthorID = set.AuthorID,
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
+ },
+ Status = BeatmapOnlineStatus.Graveyard
+ });
+
+ beatmaps.Add(new APIBeatmap
+ {
+ OnlineID = 1919,
+ DifficultyName = "Guest Diff",
+ RulesetID = Ruleset.Value.OnlineID,
+ StarRating = 8.1,
+ OverallDifficulty = 3.5f,
+ AuthorID = 3,
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
+ },
+ Status = BeatmapOnlineStatus.Graveyard
+ });
+
+ set.Beatmaps = beatmaps.ToArray();
+
+ return set;
+ }
+
private void downloadAssert(bool shown)
{
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.HeaderContent.DownloadButtonsVisible == shown);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index feab86d3ee..f094d40caa 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -1068,6 +1068,21 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("options disabled", () => !songSelect.ChildrenOfType().Single().Enabled.Value);
}
+ [Test]
+ public void TestTextBoxBeatmapDifficultyCount()
+ {
+ createSongSelect();
+
+ AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches");
+
+ addRulesetImportStep(0);
+
+ AddAssert("3 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "3 matches");
+ AddStep("delete all beatmaps", () => manager.Delete());
+ AddUntilStep("wait for no beatmap", () => Beatmap.IsDefault);
+ AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches");
+ }
+
private void waitForInitialSelection()
{
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 5ccaebd721..f99fe1d8d4 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -14,6 +14,7 @@ using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
@@ -67,6 +68,19 @@ namespace osu.Game.Tests.Visual.UserInterface
}
}
});
+ r.Add(new ModPreset
+ {
+ Name = "Half Time 0.5x",
+ Description = "Very slow",
+ Ruleset = r.Find(OsuRuleset.SHORT_NAME),
+ Mods = new[]
+ {
+ new OsuModHalfTime
+ {
+ SpeedChange = { Value = 0.5 }
+ }
+ }
+ });
});
});
}
@@ -566,6 +580,28 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("5 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 5);
}
+ [Test]
+ public void TestModMultiplierUpdates()
+ {
+ createScreen();
+
+ AddStep("select mod preset with half time", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x"));
+ InputManager.Click(MouseButton.Left);
+ });
+ AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.5));
+
+ // this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
+ // it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
+ AddStep("force collection", GC.Collect);
+
+ AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
+ AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType().Single()
+ .ChildrenOfType>().Single().TriggerClick());
+ AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.7));
+ }
+
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
() => modSelectOverlay.ChildrenOfType().Any() && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded));
diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs
index 4752a88199..4731a70753 100644
--- a/osu.Game/Beatmaps/BeatmapImporter.cs
+++ b/osu.Game/Beatmaps/BeatmapImporter.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps
protected override string[] HashableFileTypes => new[] { ".osu" };
- public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; }
+ public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
public BeatmapImporter(Storage storage, RealmAccess realm)
: base(storage, realm)
@@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps
first.PerformRead(s =>
{
// Re-run processing even in this case. We might have outdated metadata.
- ProcessBeatmap?.Invoke((s, false));
+ ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst);
});
return first;
}
@@ -206,7 +206,7 @@ namespace osu.Game.Beatmaps
protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
{
base.PostImport(model, realm, parameters);
- ProcessBeatmap?.Invoke((model, parameters.Batch));
+ ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst);
}
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 3208598f56..63e878b80d 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -167,7 +167,7 @@ namespace osu.Game.Beatmaps
///
public double DistanceSpacing { get; set; } = 1.0;
- public int BeatDivisor { get; set; }
+ public int BeatDivisor { get; set; } = 4;
public int GridSize { get; set; }
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 877a0c7667..e87e71ae33 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps
private readonly LegacyBeatmapExporter beatmapExporter;
- public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; }
+ public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
public override bool PauseImports
{
@@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
beatmapImporter = CreateBeatmapImporter(storage, realm);
- beatmapImporter.ProcessBeatmap = args => ProcessBeatmap?.Invoke(args);
+ beatmapImporter.ProcessBeatmap = (beatmapSet, scope) => ProcessBeatmap?.Invoke(beatmapSet, scope);
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
@@ -375,7 +375,7 @@ namespace osu.Game.Beatmaps
// user requested abort
return;
- var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal)));
+ var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase)));
if (video != null)
{
@@ -424,6 +424,13 @@ namespace osu.Game.Beatmaps
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent.BeatmapInfo = beatmapInfo;
+ // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this.
+ // Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file,
+ // which influences the beatmap checksums.
+ beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
+ beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
+ beatmapInfo.ResetOnlineInfo();
+
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
@@ -447,9 +454,6 @@ namespace osu.Game.Beatmaps
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash();
- beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
- beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
-
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
updateHashAndMarkDirty(setInfo);
@@ -463,7 +467,9 @@ namespace osu.Game.Beatmaps
if (transferCollections)
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
- ProcessBeatmap?.Invoke((liveBeatmapSet, false));
+ // do not look up metadata.
+ // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst.
+ ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None);
});
}
@@ -551,4 +557,11 @@ namespace osu.Game.Beatmaps
public override string HumanisedModelName => "beatmap";
}
+
+ ///
+ /// Delegate type for beatmap processing callbacks.
+ ///
+ /// The beatmap set to be processed.
+ /// The scope to use when looking up metadata.
+ public delegate void ProcessBeatmapDelegate(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope);
}
diff --git a/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs
index 98aefd75d3..b160043820 100644
--- a/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs
+++ b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps
var matchingSet = r.All().FirstOrDefault(s => s.OnlineID == id);
if (matchingSet != null)
- beatmapUpdater.Queue(matchingSet.ToLive(realm), true);
+ beatmapUpdater.Queue(matchingSet.ToLive(realm), MetadataLookupScope.OnlineFirst);
}
});
}
diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs
index d7b1fac7b3..af9f32f834 100644
--- a/osu.Game/Beatmaps/BeatmapUpdater.cs
+++ b/osu.Game/Beatmaps/BeatmapUpdater.cs
@@ -42,24 +42,25 @@ namespace osu.Game.Beatmaps
/// Queue a beatmap for background processing.
///
/// The managed beatmap set to update. A transaction will be opened to apply changes.
- /// Whether metadata from an online source should be preferred. If true, the local cache will be skipped to ensure the freshest data state possible.
- public void Queue(Live beatmapSet, bool preferOnlineFetch = false)
+ /// The preferred scope to use for metadata lookup.
+ public void Queue(Live beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
{
Logger.Log($"Queueing change for local beatmap {beatmapSet}");
- Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, preferOnlineFetch)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
+ Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
}
///
/// Run all processing on a beatmap immediately.
///
/// The managed beatmap set to update. A transaction will be opened to apply changes.
- /// Whether metadata from an online source should be preferred. If true, the local cache will be skipped to ensure the freshest data state possible.
- public void Process(BeatmapSetInfo beatmapSet, bool preferOnlineFetch = false) => beatmapSet.Realm.Write(r =>
+ /// The preferred scope to use for metadata lookup.
+ public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm.Write(r =>
{
// Before we use below, we want to invalidate.
workingBeatmapCache.Invalidate(beatmapSet);
- metadataLookup.Update(beatmapSet, preferOnlineFetch);
+ if (lookupScope != MetadataLookupScope.None)
+ metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst);
foreach (var beatmap in beatmapSet.Beatmaps)
{
diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
index c454439c5c..6cd4d74a31 100644
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public readonly Bindable SampleBankBindable = new Bindable(DEFAULT_BANK) { Default = DEFAULT_BANK };
///
- /// The speed multiplier at this control point.
+ /// The default sample bank at this control point.
///
public string SampleBank
{
@@ -39,7 +39,7 @@ namespace osu.Game.Beatmaps.ControlPoints
}
///
- /// The default sample bank at this control point.
+ /// The default sample volume at this control point.
///
public readonly BindableInt SampleVolumeBindable = new BindableInt(100)
{
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index a9bdd21b64..5e98025c9a 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -3,6 +3,8 @@
#nullable disable
+#pragma warning disable 618
+
using System;
using System.Collections.Generic;
using System.IO;
@@ -15,7 +17,9 @@ using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing;
using osu.Game.IO;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
+using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Beatmaps.Formats
{
@@ -26,6 +30,11 @@ namespace osu.Game.Beatmaps.Formats
///
public const int EARLY_VERSION_TIMING_OFFSET = 24;
+ ///
+ /// A small adjustment to the start time of control points to account for rounding/precision errors.
+ ///
+ private const double control_point_leniency = 1;
+
internal static RulesetStore RulesetStore;
private Beatmap beatmap;
@@ -85,7 +94,45 @@ namespace osu.Game.Beatmaps.Formats
this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList();
foreach (var hitObject in this.beatmap.HitObjects)
- hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.Difficulty);
+ {
+ applyDefaults(hitObject);
+ applySamples(hitObject);
+ }
+ }
+
+ private void applyDefaults(HitObject hitObject)
+ {
+ DifficultyControlPoint difficultyControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.DifficultyPointAt(hitObject.StartTime) ?? DifficultyControlPoint.DEFAULT;
+
+ if (difficultyControlPoint is LegacyDifficultyControlPoint legacyDifficultyControlPoint)
+ {
+ hitObject.LegacyBpmMultiplier = legacyDifficultyControlPoint.BpmMultiplier;
+ if (hitObject is IHasGenerateTicks hasGenerateTicks)
+ hasGenerateTicks.GenerateTicks = legacyDifficultyControlPoint.GenerateTicks;
+ }
+
+ if (hitObject is IHasSliderVelocity hasSliderVelocity)
+ hasSliderVelocity.SliderVelocity = difficultyControlPoint.SliderVelocity;
+
+ hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
+ }
+
+ private void applySamples(HitObject hitObject)
+ {
+ SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + control_point_leniency) ?? SampleControlPoint.DEFAULT;
+
+ hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
+
+ if (hitObject is IHasRepeats hasRepeats)
+ {
+ for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
+ {
+ double time = hitObject.StartTime + i * hasRepeats.Duration / hasRepeats.SpanCount() + control_point_leniency;
+ var nodeSamplePoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(time) ?? SampleControlPoint.DEFAULT;
+
+ hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(o => nodeSamplePoint.ApplyTo(o)).ToList();
+ }
+ }
}
///
@@ -369,7 +416,7 @@ namespace osu.Game.Beatmaps.Formats
// Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
// instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
// video extensions and handle similar to a background if it doesn't match.
- if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename)))
+ if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
{
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
}
@@ -451,9 +498,7 @@ namespace osu.Game.Beatmaps.Formats
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
-#pragma warning disable 618
addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength)
-#pragma warning restore 618
{
SliderVelocity = speedMultiplier,
}, timingChange);
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 072e442dea..7fbcca9adb 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -92,7 +92,8 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}"));
writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}"));
writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}"));
- writer.WriteLine(FormattableString.Invariant($"SampleSet: {toLegacySampleBank((beatmap.HitObjects.FirstOrDefault()?.SampleControlPoint ?? SampleControlPoint.DEFAULT).SampleBank)}"));
+ writer.WriteLine(FormattableString.Invariant(
+ $"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints?.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}"));
writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}"));
writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}"));
writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}"));
@@ -173,9 +174,6 @@ namespace osu.Game.Beatmaps.Formats
private void handleControlPoints(TextWriter writer)
{
- if (beatmap.ControlPointInfo.Groups.Count == 0)
- return;
-
var legacyControlPoints = new LegacyControlPointInfo();
foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
legacyControlPoints.Add(point.Time, point.DeepClone());
@@ -199,33 +197,43 @@ namespace osu.Game.Beatmaps.Formats
legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed });
}
+ LegacyControlPointProperties lastControlPointProperties = new LegacyControlPointProperties();
+
foreach (var group in legacyControlPoints.Groups)
{
var groupTimingPoint = group.ControlPoints.OfType().FirstOrDefault();
+ var controlPointProperties = getLegacyControlPointProperties(group, groupTimingPoint != null);
// If the group contains a timing control point, it needs to be output separately.
if (groupTimingPoint != null)
{
writer.Write(FormattableString.Invariant($"{groupTimingPoint.Time},"));
writer.Write(FormattableString.Invariant($"{groupTimingPoint.BeatLength},"));
- outputControlPointAt(groupTimingPoint.Time, true);
+ outputControlPointAt(controlPointProperties, true);
+ lastControlPointProperties = controlPointProperties;
+ lastControlPointProperties.SliderVelocity = 1;
}
+ if (controlPointProperties.IsRedundant(lastControlPointProperties))
+ continue;
+
// Output any remaining effects as secondary non-timing control point.
- var difficultyPoint = legacyControlPoints.DifficultyPointAt(group.Time);
writer.Write(FormattableString.Invariant($"{group.Time},"));
- writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SliderVelocity},"));
- outputControlPointAt(group.Time, false);
+ writer.Write(FormattableString.Invariant($"{-100 / controlPointProperties.SliderVelocity},"));
+ outputControlPointAt(controlPointProperties, false);
+ lastControlPointProperties = controlPointProperties;
}
- void outputControlPointAt(double time, bool isTimingPoint)
+ LegacyControlPointProperties getLegacyControlPointProperties(ControlPointGroup group, bool updateSampleBank)
{
- var samplePoint = legacyControlPoints.SamplePointAt(time);
- var effectPoint = legacyControlPoints.EffectPointAt(time);
- var timingPoint = legacyControlPoints.TimingPointAt(time);
+ var timingPoint = legacyControlPoints.TimingPointAt(group.Time);
+ var difficultyPoint = legacyControlPoints.DifficultyPointAt(group.Time);
+ var samplePoint = legacyControlPoints.SamplePointAt(group.Time);
+ var effectPoint = legacyControlPoints.EffectPointAt(group.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));
+ int customSampleBank = toLegacyCustomSampleBank(tempHitSample);
// Convert effect flags to the legacy format
LegacyEffectFlags effectFlags = LegacyEffectFlags.None;
@@ -234,12 +242,26 @@ namespace osu.Game.Beatmaps.Formats
if (timingPoint.OmitFirstBarLine)
effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
- writer.Write(FormattableString.Invariant($"{timingPoint.TimeSignature.Numerator},"));
- writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},"));
- writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},"));
- writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},"));
- writer.Write(FormattableString.Invariant($"{(isTimingPoint ? '1' : '0')},"));
- writer.Write(FormattableString.Invariant($"{(int)effectFlags}"));
+ return new LegacyControlPointProperties
+ {
+ SliderVelocity = difficultyPoint.SliderVelocity,
+ TimingSignature = timingPoint.TimeSignature.Numerator,
+ SampleBank = updateSampleBank ? (int)toLegacySampleBank(tempHitSample.Bank) : lastControlPointProperties.SampleBank,
+ // Inherit the previous custom sample bank if the current custom sample bank is not set
+ CustomSampleBank = customSampleBank >= 0 ? customSampleBank : lastControlPointProperties.CustomSampleBank,
+ SampleVolume = tempHitSample.Volume,
+ EffectFlags = effectFlags
+ };
+ }
+
+ void outputControlPointAt(LegacyControlPointProperties controlPoint, bool isTimingPoint)
+ {
+ writer.Write(FormattableString.Invariant($"{controlPoint.TimingSignature.ToString(CultureInfo.InvariantCulture)},"));
+ writer.Write(FormattableString.Invariant($"{controlPoint.SampleBank.ToString(CultureInfo.InvariantCulture)},"));
+ writer.Write(FormattableString.Invariant($"{controlPoint.CustomSampleBank.ToString(CultureInfo.InvariantCulture)},"));
+ writer.Write(FormattableString.Invariant($"{controlPoint.SampleVolume.ToString(CultureInfo.InvariantCulture)},"));
+ writer.Write(FormattableString.Invariant($"{(isTimingPoint ? "1" : "0")},"));
+ writer.Write(FormattableString.Invariant($"{((int)controlPoint.EffectFlags).ToString(CultureInfo.InvariantCulture)}"));
writer.WriteLine();
}
@@ -249,7 +271,10 @@ namespace osu.Game.Beatmaps.Formats
yield break;
foreach (var hitObject in hitObjects)
- yield return hitObject.DifficultyControlPoint;
+ {
+ if (hitObject is IHasSliderVelocity hasSliderVelocity)
+ yield return new DifficultyControlPoint { Time = hitObject.StartTime, SliderVelocity = hasSliderVelocity.SliderVelocity };
+ }
}
void extractDifficultyControlPoints(IEnumerable hitObjects)
@@ -268,7 +293,15 @@ namespace osu.Game.Beatmaps.Formats
{
foreach (var hitObject in hitObjects)
{
- yield return hitObject.SampleControlPoint;
+ if (hitObject.Samples.Count > 0)
+ {
+ int volume = hitObject.Samples.Max(o => o.Volume);
+ int customIndex = hitObject.Samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo)
+ ? hitObject.Samples.OfType().Max(o => o.CustomSampleBank)
+ : -1;
+
+ yield return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = hitObject.GetEndTime(), SampleVolume = volume, CustomSampleBank = customIndex };
+ }
foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects))
yield return nested;
@@ -466,16 +499,16 @@ namespace osu.Game.Beatmaps.Formats
if (curveData != null)
{
- for (int i = 0; i < curveData.NodeSamples.Count; i++)
+ for (int i = 0; i < curveData.SpanCount() + 1; i++)
{
- writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}"));
- writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ",");
+ writer.Write(FormattableString.Invariant($"{(i < curveData.NodeSamples.Count ? (int)toLegacyHitSoundType(curveData.NodeSamples[i]) : 0)}"));
+ writer.Write(i != curveData.SpanCount() ? "|" : ",");
}
- for (int i = 0; i < curveData.NodeSamples.Count; i++)
+ for (int i = 0; i < curveData.SpanCount() + 1; i++)
{
- writer.Write(getSampleBank(curveData.NodeSamples[i], true));
- writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ",");
+ writer.Write(i < curveData.NodeSamples.Count ? getSampleBank(curveData.NodeSamples[i], true) : "0:0");
+ writer.Write(i != curveData.SpanCount() ? "|" : ",");
}
}
}
@@ -506,10 +539,18 @@ namespace osu.Game.Beatmaps.Formats
if (!banksOnly)
{
- string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name)));
+ int customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name)));
string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty;
int volume = samples.FirstOrDefault()?.Volume ?? 100;
+ // We want to ignore custom sample banks and volume when not encoding to the mania game mode,
+ // because they cause unexpected results in the editor and are already satisfied by the control points.
+ if (onlineRulesetID != 3)
+ {
+ customSampleBank = 0;
+ volume = 0;
+ }
+
sb.Append(':');
sb.Append(FormattableString.Invariant($"{customSampleBank}:"));
sb.Append(FormattableString.Invariant($"{volume}:"));
@@ -562,12 +603,30 @@ namespace osu.Game.Beatmaps.Formats
}
}
- private string toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo)
+ private int toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo)
{
if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy)
- return legacy.CustomSampleBank.ToString(CultureInfo.InvariantCulture);
+ return legacy.CustomSampleBank;
- return "0";
+ return 0;
+ }
+
+ private struct LegacyControlPointProperties
+ {
+ internal double SliderVelocity { get; set; }
+ internal int TimingSignature { get; init; }
+ internal int SampleBank { get; init; }
+ internal int CustomSampleBank { get; init; }
+ internal int SampleVolume { get; init; }
+ internal LegacyEffectFlags EffectFlags { get; init; }
+
+ internal bool IsRedundant(LegacyControlPointProperties other) =>
+ SliderVelocity == other.SliderVelocity &&
+ TimingSignature == other.TimingSignature &&
+ SampleBank == other.SampleBank &&
+ CustomSampleBank == other.CustomSampleBank &&
+ SampleVolume == other.SampleVolume &&
+ EffectFlags == other.EffectFlags;
}
}
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
index f8308fe431..df5d3edb55 100644
--- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
@@ -114,7 +114,7 @@ namespace osu.Game.Beatmaps.Formats
//
// This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video
// (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451).
- if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path)))
+ if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant()))
break;
storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset));
diff --git a/osu.Game/Beatmaps/MetadataLookupScope.cs b/osu.Game/Beatmaps/MetadataLookupScope.cs
new file mode 100644
index 0000000000..e1fbedc26a
--- /dev/null
+++ b/osu.Game/Beatmaps/MetadataLookupScope.cs
@@ -0,0 +1,26 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Beatmaps
+{
+ ///
+ /// Determines which sources (if any at all) should be queried in which order for a beatmap's metadata.
+ ///
+ public enum MetadataLookupScope
+ {
+ ///
+ /// Do not attempt to look up the beatmap metadata either in the local cache or online.
+ ///
+ None,
+
+ ///
+ /// Try the local metadata cache first before querying online sources.
+ ///
+ LocalCacheFirst,
+
+ ///
+ /// Query online sources immediately.
+ ///
+ OnlineFirst
+ }
+}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 70ad6bfc96..1e7d3cf84f 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -157,6 +157,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.Scaling, ScalingMode.Off);
SetDefault(OsuSetting.SafeAreaConsiderations, true);
+ SetDefault(OsuSetting.ScalingBackgroundDim, 0.9f, 0.5f, 1f);
SetDefault(OsuSetting.ScalingSizeX, 0.8f, 0.2f, 1f);
SetDefault(OsuSetting.ScalingSizeY, 0.8f, 0.2f, 1f);
@@ -364,6 +365,7 @@ namespace osu.Game.Configuration
ScalingPositionY,
ScalingSizeX,
ScalingSizeY,
+ ScalingBackgroundDim,
UIScale,
IntroSequence,
NotifyOnUsernameMentioned,
diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs
index fb5c3e3b60..c47aba2f0c 100644
--- a/osu.Game/Graphics/Containers/ScalingContainer.cs
+++ b/osu.Game/Graphics/Containers/ScalingContainer.cs
@@ -15,6 +15,7 @@ using osu.Game.Configuration;
using osu.Game.Screens;
using osu.Game.Screens.Backgrounds;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Graphics.Containers
{
@@ -46,6 +47,8 @@ namespace osu.Game.Graphics.Containers
private BackgroundScreenStack backgroundStack;
+ private Bindable scalingMenuBackgroundDim;
+
private RectangleF? customRect;
private bool customRectIsRelativePosition;
@@ -138,6 +141,9 @@ namespace osu.Game.Graphics.Containers
safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy();
safeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize));
+
+ scalingMenuBackgroundDim = config.GetBindable(OsuSetting.ScalingBackgroundDim);
+ scalingMenuBackgroundDim.ValueChanged += _ => Scheduler.AddOnce(updateSize);
}
protected override void LoadComplete()
@@ -148,7 +154,9 @@ namespace osu.Game.Graphics.Containers
sizableContainer.FinishTransforms();
}
- private bool requiresBackgroundVisible => (scalingMode.Value == ScalingMode.Everything || scalingMode.Value == ScalingMode.ExcludeOverlays) && (sizeX.Value != 1 || sizeY.Value != 1);
+ private bool requiresBackgroundVisible => (scalingMode.Value == ScalingMode.Everything || scalingMode.Value == ScalingMode.ExcludeOverlays)
+ && (sizeX.Value != 1 || sizeY.Value != 1)
+ && scalingMenuBackgroundDim.Value < 1;
private void updateSize()
{
@@ -161,8 +169,8 @@ namespace osu.Game.Graphics.Containers
{
AddInternal(backgroundStack = new BackgroundScreenStack
{
- Colour = OsuColour.Gray(0.1f),
Alpha = 0,
+ Colour = Color4.Black,
Depth = float.MaxValue
});
@@ -170,6 +178,7 @@ namespace osu.Game.Graphics.Containers
}
backgroundStack.FadeIn(TRANSITION_DURATION);
+ backgroundStack.FadeColour(OsuColour.Gray(1.0f - scalingMenuBackgroundDim.Value), TRANSITION_DURATION, Easing.OutQuint);
}
else
backgroundStack?.FadeOut(TRANSITION_DURATION);
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index 0c36d73085..0e26029ffa 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -97,7 +97,9 @@ namespace osu.Game.Graphics.UserInterface
// Find the number of significant digits (we could have less than 5 after normalize())
int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
- return floatValue.ToString($"N{significantDigits}");
+ string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty;
+
+ return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}";
}
///
diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
index fa58ae27f2..c9e1f74917 100644
--- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
@@ -49,11 +49,10 @@ namespace osu.Game.Graphics.UserInterface
private const float transition_length = 500;
private Sample sampleChecked;
private Sample sampleUnchecked;
+ private readonly SpriteIcon icon;
public OsuTabControlCheckbox()
{
- SpriteIcon icon;
-
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
@@ -85,14 +84,6 @@ namespace osu.Game.Graphics.UserInterface
Anchor = Anchor.BottomLeft,
}
};
-
- Current.ValueChanged += selected =>
- {
- icon.Icon = selected.NewValue ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.Circle;
- text.Font = text.Font.With(weight: selected.NewValue ? FontWeight.Bold : FontWeight.Medium);
-
- updateFade();
- };
}
[BackgroundDependencyLoader]
@@ -105,6 +96,19 @@ namespace osu.Game.Graphics.UserInterface
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Current.BindValueChanged(selected =>
+ {
+ icon.Icon = selected.NewValue ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.Circle;
+ text.Font = text.Font.With(weight: selected.NewValue ? FontWeight.Bold : FontWeight.Medium);
+
+ updateFade();
+ }, true);
+ }
+
protected override bool OnHover(HoverEvent e)
{
updateFade();
diff --git a/osu.Game/Graphics/UserInterface/TimeSlider.cs b/osu.Game/Graphics/UserInterface/TimeSlider.cs
index e4058827f3..e6e7ae9305 100644
--- a/osu.Game/Graphics/UserInterface/TimeSlider.cs
+++ b/osu.Game/Graphics/UserInterface/TimeSlider.cs
@@ -12,6 +12,6 @@ namespace osu.Game.Graphics.UserInterface
///
public partial class TimeSlider : RoundedSliderBar
{
- public override LocalisableString TooltipText => $"{Current.Value:N0} ms";
+ public override LocalisableString TooltipText => $"{base.TooltipText} ms";
}
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
index 37e15c6127..7097102335 100644
--- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
get
{
- if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension))
+ if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension.ToLowerInvariant()))
return FontAwesome.Regular.FileVideo;
switch (File.Extension)
diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs
index f4e23ae7cb..20258b9c35 100644
--- a/osu.Game/Localisation/EditorStrings.cs
+++ b/osu.Game/Localisation/EditorStrings.cs
@@ -99,6 +99,16 @@ namespace osu.Game.Localisation
///
public static LocalisableString TimelineTicks => new TranslatableString(getKey(@"timeline_ticks"), @"Ticks");
+ ///
+ /// "{0:0}°"
+ ///
+ public static LocalisableString RotationUnsnapped(float newRotation) => new TranslatableString(getKey(@"rotation_unsnapped"), @"{0:0}°", newRotation);
+
+ ///
+ /// "{0:0}° (snapped)"
+ ///
+ public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation);
+
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
diff --git a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs
index 3a7fe4bb12..a77ee066e4 100644
--- a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs
+++ b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs
@@ -15,9 +15,9 @@ namespace osu.Game.Localisation
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Obtaining Beatmaps");
///
- /// ""Beatmaps" are what we call playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection."
+ /// ""Beatmaps" are what we call sets of playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection."
///
- public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"""Beatmaps"" are what we call playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection.");
+ public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"""Beatmaps"" are what we call sets of playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection.");
///
/// "If you are a new player, we recommend playing through the tutorial to get accustomed to the gameplay."
diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs
index 1b0df6ecf6..52e6a5eaac 100644
--- a/osu.Game/Localisation/RulesetSettingsStrings.cs
+++ b/osu.Game/Localisation/RulesetSettingsStrings.cs
@@ -29,6 +29,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString CursorTrail => new TranslatableString(getKey(@"cursor_trail"), @"Cursor trail");
+ ///
+ /// "Cursor ripples"
+ ///
+ public static LocalisableString CursorRipples => new TranslatableString(getKey(@"cursor_ripples"), @"Cursor ripples");
+
///
/// "Playfield border style"
///
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 8f27e5dc53..34e31b0d61 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -310,7 +310,7 @@ namespace osu.Game
base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
- BeatmapManager.ProcessBeatmap = args => beatmapUpdater.Process(args.beatmapSet, !args.isBatch);
+ BeatmapManager.ProcessBeatmap = (beatmapSet, scope) => beatmapUpdater.Process(beatmapSet, scope);
dependencies.Cache(userCache = new UserLookupCache());
base.Content.Add(userCache);
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
index 23de1cf76d..3fa0fc7a77 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
@@ -107,7 +107,7 @@ namespace osu.Game.Overlays.BeatmapListing
Padding = new MarginPadding
{
Vertical = 20,
- Horizontal = 40,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
},
Child = new FillFlowContainer
{
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
index 585e0dd1a2..104f861df7 100644
--- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
@@ -31,6 +31,7 @@ namespace osu.Game.Overlays.BeatmapSet
private const float tile_spacing = 2;
private readonly OsuSpriteText version, starRating, starRatingText;
+ private readonly LinkFlowContainer guestMapperContainer;
private readonly FillFlowContainer starRatingContainer;
private readonly Statistic plays, favourites;
@@ -88,6 +89,14 @@ namespace osu.Game.Overlays.BeatmapSet
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold)
},
+ guestMapperContainer = new LinkFlowContainer(s =>
+ s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11))
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Margin = new MarginPadding { Bottom = 1 },
+ },
starRatingContainer = new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
@@ -198,8 +207,21 @@ namespace osu.Game.Overlays.BeatmapSet
updateDifficultyButtons();
}
- private void showBeatmap(IBeatmapInfo? beatmapInfo)
+ private void showBeatmap(APIBeatmap? beatmapInfo)
{
+ guestMapperContainer.Clear();
+
+ if (beatmapInfo?.AuthorID != BeatmapSet?.AuthorID)
+ {
+ APIUser? user = BeatmapSet?.RelatedUsers?.SingleOrDefault(u => u.OnlineID == beatmapInfo?.AuthorID);
+
+ if (user != null)
+ {
+ guestMapperContainer.AddText("mapped by ");
+ guestMapperContainer.AddUserLink(user);
+ }
+ }
+
version.Text = beatmapInfo?.DifficultyName ?? string.Empty;
}
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
index 26e6b1f158..7ff8352054 100644
--- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
@@ -97,8 +97,8 @@ namespace osu.Game.Overlays.BeatmapSet
Padding = new MarginPadding
{
Vertical = BeatmapSetOverlay.Y_PADDING,
- Left = BeatmapSetOverlay.X_PADDING,
- Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
+ Left = WaveOverlayContainer.HORIZONTAL_PADDING,
+ Right = WaveOverlayContainer.HORIZONTAL_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
},
Children = new Drawable[]
{
@@ -170,7 +170,7 @@ namespace osu.Game.Overlays.BeatmapSet
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = BeatmapSetOverlay.X_PADDING },
+ Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = WaveOverlayContainer.HORIZONTAL_PADDING },
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs
index 58739eb471..8758b9c5cf 100644
--- a/osu.Game/Overlays/BeatmapSet/Info.cs
+++ b/osu.Game/Overlays/BeatmapSet/Info.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Overlays.BeatmapSet
new Container
{
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Top = 15, Horizontal = BeatmapSetOverlay.X_PADDING },
+ Padding = new MarginPadding { Top = 15, Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Children = new Drawable[]
{
new Container
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
index 9eb04d9cc5..6d89313979 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
@@ -116,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
- Padding = new MarginPadding { Horizontal = 50 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Margin = new MarginPadding { Vertical = 20 },
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs
index 237ce22767..873336bb6e 100644
--- a/osu.Game/Overlays/BeatmapSetOverlay.cs
+++ b/osu.Game/Overlays/BeatmapSetOverlay.cs
@@ -25,7 +25,6 @@ namespace osu.Game.Overlays
{
public partial class BeatmapSetOverlay : OnlineOverlay
{
- public const float X_PADDING = 40;
public const float Y_PADDING = 25;
public const float RIGHT_WIDTH = 275;
diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
index 96d5203d14..08978ac2ab 100644
--- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
@@ -18,8 +18,6 @@ namespace osu.Game.Overlays.Changelog
{
public partial class ChangelogBuild : FillFlowContainer
{
- public const float HORIZONTAL_PADDING = 70;
-
public Action SelectBuild;
protected readonly APIChangelogBuild Build;
@@ -33,7 +31,7 @@ namespace osu.Game.Overlays.Changelog
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
- Padding = new MarginPadding { Horizontal = HORIZONTAL_PADDING };
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING };
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs
index 54ada24987..e9be67e977 100644
--- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs
@@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Changelog
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
- Horizontal = 65,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - ChangelogUpdateStreamItem.PADDING,
Vertical = 20
},
Child = Streams = new ChangelogUpdateStreamControl { Current = currentStream },
diff --git a/osu.Game/Overlays/Changelog/ChangelogListing.cs b/osu.Game/Overlays/Changelog/ChangelogListing.cs
index d7c9ff67fe..4b784c7a28 100644
--- a/osu.Game/Overlays/Changelog/ChangelogListing.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogListing.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Changelog
{
RelativeSizeAxes = Axes.X,
Height = 1,
- Padding = new MarginPadding { Horizontal = ChangelogBuild.HORIZONTAL_PADDING },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Margin = new MarginPadding { Top = 30 },
Child = new Box
{
diff --git a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs
index 04526eb7ba..4aded1dd59 100644
--- a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Changelog
Padding = new MarginPadding
{
Vertical = 20,
- Horizontal = 50,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
};
}
@@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Changelog
Direction = FillDirection.Vertical,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
- Padding = new MarginPadding { Right = 50 + image_container_width },
+ Padding = new MarginPadding { Right = WaveOverlayContainer.HORIZONTAL_PADDING + image_container_width },
Children = new Drawable[]
{
new OsuSpriteText
diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs
index c4e4700674..24536fe460 100644
--- a/osu.Game/Overlays/Comments/CommentsContainer.cs
+++ b/osu.Game/Overlays/Comments/CommentsContainer.cs
@@ -99,7 +99,7 @@ namespace osu.Game.Overlays.Comments
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = 50, Vertical = 20 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 20 },
Children = new Drawable[]
{
avatar = new UpdateableAvatar(api.LocalUser.Value)
@@ -152,7 +152,7 @@ namespace osu.Game.Overlays.Comments
ShowDeleted = { BindTarget = ShowDeleted },
Margin = new MarginPadding
{
- Horizontal = 70,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
Vertical = 10
}
},
@@ -393,7 +393,7 @@ namespace osu.Game.Overlays.Comments
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
- Margin = new MarginPadding { Left = 50 },
+ Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING },
Text = CommentsStrings.Empty
}
});
diff --git a/osu.Game/Overlays/Comments/CommentsHeader.cs b/osu.Game/Overlays/Comments/CommentsHeader.cs
index e6d44e618b..0ae1f839a1 100644
--- a/osu.Game/Overlays/Comments/CommentsHeader.cs
+++ b/osu.Game/Overlays/Comments/CommentsHeader.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Comments
new Container
{
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Horizontal = 50 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Children = new Drawable[]
{
new OverlaySortTabControl
diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs
index 397dd46cdc..a710406548 100644
--- a/osu.Game/Overlays/Comments/DrawableComment.cs
+++ b/osu.Game/Overlays/Comments/DrawableComment.cs
@@ -537,7 +537,7 @@ namespace osu.Game.Overlays.Comments
{
return new MarginPadding
{
- Horizontal = 70,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
Vertical = 15
};
}
diff --git a/osu.Game/Overlays/Comments/TotalCommentsCounter.cs b/osu.Game/Overlays/Comments/TotalCommentsCounter.cs
index 38928f6f3d..2065f7a76b 100644
--- a/osu.Game/Overlays/Comments/TotalCommentsCounter.cs
+++ b/osu.Game/Overlays/Comments/TotalCommentsCounter.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Comments
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
- Margin = new MarginPadding { Left = 50 },
+ Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING },
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
index 1540aa8fbb..5047992c8b 100644
--- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Overlays.Dashboard
new Container
{
RelativeSizeAxes = Axes.X,
- Padding = new MarginPadding(padding),
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = padding },
Child = searchTextBox = new BasicSearchTextBox
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
index 73fab6d62b..e3accfd2ad 100644
--- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
Padding = new MarginPadding
{
Top = 20,
- Horizontal = 45
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - FriendsOnlineStatusItem.PADDING
},
Child = onlineStreamControl = new FriendOnlineStreamControl(),
}
@@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = 50 }
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }
},
loading = new LoadingLayer(true)
}
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 16602db4be..38ae8c68cb 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -242,17 +242,21 @@ namespace osu.Game.Overlays.Mods
if (AllowCustomisation)
((IBindable>)modSettingsArea.SelectedMods).BindTo(SelectedMods);
- SelectedMods.BindValueChanged(val =>
+ SelectedMods.BindValueChanged(_ =>
{
- modSettingChangeTracker?.Dispose();
-
updateMultiplier();
updateFromExternalSelection();
updateCustomisation();
+ modSettingChangeTracker?.Dispose();
+
if (AllowCustomisation)
{
- modSettingChangeTracker = new ModSettingChangeTracker(val.NewValue);
+ // Importantly, use SelectedMods.Value here (and not the ValueChanged NewValue) as the latter can
+ // potentially be stale, due to complexities in the way change trackers work.
+ //
+ // See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988
+ modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value);
modSettingChangeTracker.SettingChanged += _ => updateMultiplier();
}
}, true);
diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs
index 00c5ce8002..90fdfd0491 100644
--- a/osu.Game/Overlays/Music/PlaylistItem.cs
+++ b/osu.Game/Overlays/Music/PlaylistItem.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Music
var artist = new RomanisableString(metadata.ArtistUnicode, metadata.Artist);
titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular));
- titlePart.DrawablePartsRecreated += _ => updateSelectionState(true);
+ titlePart.DrawablePartsRecreated += _ => updateSelectionState(SelectedSet.Value, applyImmediately: true);
text.AddText(@" "); // to separate the title from the artist.
text.AddText(artist, sprite =>
@@ -66,27 +66,25 @@ namespace osu.Game.Overlays.Music
sprite.Padding = new MarginPadding { Top = 1 };
});
- SelectedSet.BindValueChanged(set =>
- {
- bool newSelected = set.NewValue?.Equals(Model) == true;
-
- if (newSelected == selected)
- return;
-
- selected = newSelected;
- updateSelectionState(false);
- });
-
- updateSelectionState(true);
+ SelectedSet.BindValueChanged(set => updateSelectionState(set.NewValue));
+ updateSelectionState(SelectedSet.Value, applyImmediately: true);
});
}
private bool selected;
- private void updateSelectionState(bool instant)
+ private void updateSelectionState(Live selectedSet, bool applyImmediately = false)
{
+ bool wasSelected = selected;
+ selected = selectedSet?.Equals(Model) == true;
+
+ // Immediate updates should forcibly set correct state regardless of previous state.
+ // This ensures that the initial state is correctly applied.
+ if (wasSelected == selected && !applyImmediately)
+ return;
+
foreach (Drawable s in titlePart.Drawables)
- s.FadeColour(selected ? colours.Yellow : Color4.White, instant ? 0 : FADE_DURATION);
+ s.FadeColour(selected ? colours.Yellow : Color4.White, applyImmediately ? 0 : FADE_DURATION);
}
protected override Drawable CreateContent() => new DelayedLoadWrapper(text = new OsuTextFlowContainer
diff --git a/osu.Game/Overlays/News/Displays/ArticleListing.cs b/osu.Game/Overlays/News/Displays/ArticleListing.cs
index b6ce16ae7d..4fc9dde156 100644
--- a/osu.Game/Overlays/News/Displays/ArticleListing.cs
+++ b/osu.Game/Overlays/News/Displays/ArticleListing.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Overlays.News.Displays
{
Vertical = 20,
Left = 30,
- Right = 50
+ Right = WaveOverlayContainer.HORIZONTAL_PADDING
};
InternalChild = new FillFlowContainer
diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs
index f28d40c429..93de463204 100644
--- a/osu.Game/Overlays/OverlayHeader.cs
+++ b/osu.Game/Overlays/OverlayHeader.cs
@@ -89,7 +89,7 @@ namespace osu.Game.Overlays
}
});
- ContentSidePadding = 50;
+ ContentSidePadding = WaveOverlayContainer.HORIZONTAL_PADDING;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs
index 9752e04f44..2a615f0e12 100644
--- a/osu.Game/Overlays/OverlayScrollContainer.cs
+++ b/osu.Game/Overlays/OverlayScrollContainer.cs
@@ -185,6 +185,12 @@ namespace osu.Game.Overlays
content.ScaleTo(1, 1000, Easing.OutElastic);
base.OnMouseUp(e);
}
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ base.OnHover(e);
+ return true;
+ }
}
}
}
diff --git a/osu.Game/Overlays/OverlaySidebar.cs b/osu.Game/Overlays/OverlaySidebar.cs
index b8c0032e87..93e5e83ffc 100644
--- a/osu.Game/Overlays/OverlaySidebar.cs
+++ b/osu.Game/Overlays/OverlaySidebar.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Overlays
Padding = new MarginPadding
{
Vertical = 20,
- Left = 50,
+ Left = WaveOverlayContainer.HORIZONTAL_PADDING,
Right = 30
},
Child = CreateContent()
diff --git a/osu.Game/Overlays/OverlayStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs
index 9b18e5cccf..45181c13e4 100644
--- a/osu.Game/Overlays/OverlayStreamItem.cs
+++ b/osu.Game/Overlays/OverlayStreamItem.cs
@@ -39,12 +39,14 @@ namespace osu.Game.Overlays
private FillFlowContainer text;
private ExpandingBar expandingBar;
+ public const float PADDING = 5;
+
protected OverlayStreamItem(T value)
: base(value)
{
Height = 50;
Width = 90;
- Margin = new MarginPadding(5);
+ Margin = new MarginPadding(PADDING);
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs
index 508041eb76..24be6ce2f5 100644
--- a/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Profile.Header
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10, 10),
- Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Top = 10 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Top = 10 },
}
};
}
diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
index 1e80257a57..08a816930e 100644
--- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Overlays.Profile.Header
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
- Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 },
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
index 0dab4d582d..d964364510 100644
--- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Profile.Header
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Vertical = 10 },
- Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN },
+ Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING },
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
@@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Profile.Header
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Right = UserProfileOverlay.CONTENT_X_MARGIN },
+ Margin = new MarginPadding { Right = WaveOverlayContainer.HORIZONTAL_PADDING },
Children = new Drawable[]
{
levelBadge = new LevelBadge
@@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Profile.Header
Origin = Anchor.CentreRight,
Width = 200,
Height = 6,
- Margin = new MarginPadding { Right = 50 },
+ Margin = new MarginPadding { Right = WaveOverlayContainer.HORIZONTAL_PADDING },
Child = new LevelProgressBar
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
index 1cc3aae735..1f35f39b49 100644
--- a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Profile.Header
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
index 2f4f49788f..de678cb5d1 100644
--- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
@@ -31,6 +32,9 @@ namespace osu.Game.Overlays.Profile.Header
[Resolved]
private IAPIProvider api { get; set; } = null!;
+ [Resolved]
+ private RankingsOverlay? rankingsOverlay { get; set; }
+
private UserCoverBackground cover = null!;
private SupporterIcon supporterTag = null!;
private UpdateableAvatar avatar = null!;
@@ -38,6 +42,7 @@ namespace osu.Game.Overlays.Profile.Header
private ExternalLinkButton openUserExternally = null!;
private OsuSpriteText titleText = null!;
private UpdateableFlag userFlag = null!;
+ private OsuHoverContainer userCountryContainer = null!;
private OsuSpriteText userCountryText = null!;
private GroupBadgeFlow groupBadgeFlow = null!;
private ToggleCoverButton coverToggle = null!;
@@ -83,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Header
Direction = FillDirection.Horizontal,
Padding = new MarginPadding
{
- Left = UserProfileOverlay.CONTENT_X_MARGIN,
+ Left = WaveOverlayContainer.HORIZONTAL_PADDING,
Vertical = vertical_padding
},
Height = content_height + 2 * vertical_padding,
@@ -156,13 +161,17 @@ namespace osu.Game.Overlays.Profile.Header
Size = new Vector2(28, 20),
ShowPlaceholderOnUnknown = false,
},
- userCountryText = new OsuSpriteText
+ userCountryContainer = new OsuHoverContainer
{
- Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
- Margin = new MarginPadding { Left = 5 },
- Origin = Anchor.CentreLeft,
+ AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
- }
+ Origin = Anchor.CentreLeft,
+ Margin = new MarginPadding { Left = 5 },
+ Child = userCountryText = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
+ },
+ },
}
},
}
@@ -202,6 +211,7 @@ namespace osu.Game.Overlays.Profile.Header
openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}";
userFlag.CountryCode = user?.CountryCode ?? default;
userCountryText.Text = (user?.CountryCode ?? default).GetDescription();
+ userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default);
supporterTag.SupportLevel = user?.SupportLevel ?? 0;
titleText.Text = user?.Title ?? string.Empty;
titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff");
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index 363eb5d58e..80d48ae09e 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Profile
public ProfileHeader()
{
- ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN;
+ ContentSidePadding = WaveOverlayContainer.HORIZONTAL_PADDING;
TabControl.AddItem(LayoutStrings.HeaderUsersShow);
diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs
index 4ac86924f8..a8a240ddde 100644
--- a/osu.Game/Overlays/Profile/ProfileSection.cs
+++ b/osu.Game/Overlays/Profile/ProfileSection.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Profile
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding
{
- Horizontal = UserProfileOverlay.CONTENT_X_MARGIN - outer_gutter_width,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - outer_gutter_width,
Top = 20,
Bottom = 20,
},
@@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Profile
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding
{
- Horizontal = UserProfileOverlay.CONTENT_X_MARGIN - outer_gutter_width,
+ Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - outer_gutter_width,
Bottom = 20
}
},
diff --git a/osu.Game/Overlays/Rankings/CountryFilter.cs b/osu.Game/Overlays/Rankings/CountryFilter.cs
index e27fa7c7bd..525816f8fd 100644
--- a/osu.Game/Overlays/Rankings/CountryFilter.cs
+++ b/osu.Game/Overlays/Rankings/CountryFilter.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Rankings
Origin = Anchor.CentreLeft,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
- Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN },
+ Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING },
Children = new Drawable[]
{
new OsuSpriteText
diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs
index 31273e3b01..190da04a5d 100644
--- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs
+++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs
@@ -63,7 +63,7 @@ namespace osu.Game.Overlays.Rankings
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN },
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
index affd9a2c44..27d894cdc2 100644
--- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
@@ -23,7 +23,6 @@ namespace osu.Game.Overlays.Rankings.Tables
public abstract partial class RankingsTable : TableContainer
{
protected const int TEXT_SIZE = 12;
- private const float horizontal_inset = 20;
private const float row_height = 32;
private const float row_spacing = 3;
private const int items_per_page = 50;
@@ -39,7 +38,7 @@ namespace osu.Game.Overlays.Rankings.Tables
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- Padding = new MarginPadding { Horizontal = horizontal_inset };
+ Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING };
RowSize = new Dimension(GridSizeMode.Absolute, row_height + row_spacing);
}
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 2765d2b437..2e26d15105 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -30,6 +30,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
protected override LocalisableString Header => GraphicsSettingsStrings.LayoutHeader;
private FillFlowContainer> scalingSettings = null!;
+ private SettingsSlider dimSlider = null!;
private readonly Bindable currentDisplay = new Bindable();
@@ -57,6 +58,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private Bindable scalingSizeX = null!;
private Bindable scalingSizeY = null!;
+ private Bindable scalingBackgroundDim = null!;
+
private const int transition_duration = 400;
[BackgroundDependencyLoader]
@@ -70,6 +73,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingSizeY = osuConfig.GetBindable(OsuSetting.ScalingSizeY);
scalingPositionX = osuConfig.GetBindable(OsuSetting.ScalingPositionX);
scalingPositionY = osuConfig.GetBindable(OsuSetting.ScalingPositionY);
+ scalingBackgroundDim = osuConfig.GetBindable(OsuSetting.ScalingBackgroundDim);
if (window != null)
{
@@ -161,6 +165,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
+ dimSlider = new SettingsSlider
+ {
+ LabelText = GameplaySettingsStrings.BackgroundDim,
+ Current = scalingBackgroundDim,
+ KeyboardStep = 0.01f,
+ DisplayAsPercentage = true,
+ },
}
},
};
@@ -216,8 +227,15 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingSettings.AutoSizeAxes = scalingMode.Value != ScalingMode.Off ? Axes.Y : Axes.None;
scalingSettings.ForEach(s =>
{
- s.TransferValueOnCommit = scalingMode.Value == ScalingMode.Everything;
- s.CanBeShown.Value = scalingMode.Value != ScalingMode.Off;
+ if (s == dimSlider)
+ {
+ s.CanBeShown.Value = scalingMode.Value == ScalingMode.Everything || scalingMode.Value == ScalingMode.ExcludeOverlays;
+ }
+ else
+ {
+ s.TransferValueOnCommit = scalingMode.Value == ScalingMode.Everything;
+ s.CanBeShown.Value = scalingMode.Value != ScalingMode.Off;
+ }
});
}
}
@@ -256,7 +274,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
return;
}
- if (host.Window is WindowsWindow)
+ if (host.Renderer is IWindowsRenderer)
{
switch (fullscreenCapability.Value)
{
diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index c5f8a820ea..d1fe877e55 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -45,8 +45,6 @@ namespace osu.Game.Overlays
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
- public const float CONTENT_X_MARGIN = 50;
-
public UserProfileOverlay()
: base(OverlayColourScheme.Pink)
{
@@ -184,7 +182,7 @@ namespace osu.Game.Overlays
public ProfileSectionTabControl()
{
Height = 40;
- Padding = new MarginPadding { Horizontal = CONTENT_X_MARGIN };
+ Padding = new MarginPadding { Horizontal = HORIZONTAL_PADDING };
TabContainer.Spacing = new Vector2(20);
}
diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs
index d25f6a9ae5..00474cc0d8 100644
--- a/osu.Game/Overlays/WaveOverlayContainer.cs
+++ b/osu.Game/Overlays/WaveOverlayContainer.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Overlays
protected override string PopInSampleName => "UI/wave-pop-in";
+ public const float HORIZONTAL_PADDING = 50;
+
protected WaveOverlayContainer()
{
AddInternal(Waves = new WaveContainer
diff --git a/osu.Game/Overlays/Wiki/WikiArticlePage.cs b/osu.Game/Overlays/Wiki/WikiArticlePage.cs
index 6c1dbe3181..342a395871 100644
--- a/osu.Game/Overlays/Wiki/WikiArticlePage.cs
+++ b/osu.Game/Overlays/Wiki/WikiArticlePage.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Wiki
{
Vertical = 20,
Left = 30,
- Right = 50,
+ Right = WaveOverlayContainer.HORIZONTAL_PADDING,
},
OnAddHeading = sidebar.AddEntry,
}
diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs
index 88dc2cd7a4..2444aa4fa2 100644
--- a/osu.Game/Overlays/WikiOverlay.cs
+++ b/osu.Game/Overlays/WikiOverlay.cs
@@ -145,7 +145,7 @@ namespace osu.Game.Overlays
Padding = new MarginPadding
{
Vertical = 20,
- Horizontal = 50,
+ Horizontal = HORIZONTAL_PADDING,
},
});
}
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs
index 5b59a81f91..a2ae1764dd 100644
--- a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs
+++ b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Edit.Checks
yield break;
// Samples that allow themselves to be overridden by control points have a volume of 0.
- int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume);
+ int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume);
double samplePlayTime = sampledHitObject.GetEndTime();
EdgeType edgeType = getEdgeAtTime(hitObject, samplePlayTime);
diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
index 0df481737e..a8972775de 100644
--- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
@@ -11,8 +11,6 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
@@ -25,6 +23,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.OSD;
using osu.Game.Overlays.Settings.Sections;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.TernaryButtons;
namespace osu.Game.Rulesets.Edit
@@ -47,8 +46,6 @@ namespace osu.Game.Rulesets.Edit
IBindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
- protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; }
-
private ExpandableSlider> distanceSpacingSlider;
private ExpandableButton currentDistanceSpacingButton;
@@ -67,47 +64,29 @@ namespace osu.Game.Rulesets.Edit
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
- AddInternal(new Container
+ RightToolbox.Add(new EditorToolboxGroup("snapping")
{
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- RelativeSizeAxes = Axes.Y,
- AutoSizeAxes = Axes.X,
+ Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
Children = new Drawable[]
{
- new Box
+ distanceSpacingSlider = new ExpandableSlider>
{
- Colour = colourProvider.Background5,
- RelativeSizeAxes = Axes.Both,
+ KeyboardStep = adjust_step,
+ // Manual binding in LoadComplete to handle one-way event flow.
+ Current = DistanceSpacingMultiplier.GetUnboundCopy(),
},
- RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250)
+ currentDistanceSpacingButton = new ExpandableButton
{
- Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
- Child = new EditorToolboxGroup("snapping")
+ Action = () =>
{
- Children = new Drawable[]
- {
- distanceSpacingSlider = new ExpandableSlider>
- {
- KeyboardStep = adjust_step,
- // Manual binding in LoadComplete to handle one-way event flow.
- Current = DistanceSpacingMultiplier.GetUnboundCopy(),
- },
- currentDistanceSpacingButton = new ExpandableButton
- {
- Action = () =>
- {
- (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime();
+ (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime();
- Debug.Assert(objects != null);
+ Debug.Assert(objects != null);
- DistanceSpacingMultiplier.Value = ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after);
- DistanceSnapToggle.Value = TernaryState.True;
- },
- RelativeSizeAxes = Axes.X,
- }
- }
- }
+ DistanceSpacingMultiplier.Value = ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after);
+ DistanceSnapToggle.Value = TernaryState.True;
+ },
+ RelativeSizeAxes = Axes.X,
}
}
});
@@ -115,7 +94,7 @@ namespace osu.Game.Rulesets.Edit
private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime()
{
- HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime <= EditorClock.CurrentTime)?.HitObject;
+ HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < EditorClock.CurrentTime)?.HitObject;
if (lastBefore == null)
return null;
@@ -261,7 +240,8 @@ namespace osu.Game.Rulesets.Edit
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true)
{
- return (float)(100 * (useReferenceSliderVelocity ? referenceObject.DifficultyControlPoint.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 / BeatSnapProvider.BeatDivisor);
+ return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1
+ / BeatSnapProvider.BeatDivisor);
}
public virtual float DurationToDistance(HitObject referenceObject, double duration)
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index aee86fd942..e2dbd2acdc 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -58,8 +58,15 @@ namespace osu.Game.Rulesets.Edit
[Resolved]
protected IBeatSnapProvider BeatSnapProvider { get; private set; }
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
+
protected ComposeBlueprintContainer BlueprintContainer { get; private set; }
+ protected ExpandingToolboxContainer LeftToolbox { get; private set; }
+
+ protected ExpandingToolboxContainer RightToolbox { get; private set; }
+
private DrawableEditorRulesetWrapper drawableRulesetWrapper;
protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both };
@@ -82,7 +89,7 @@ namespace osu.Game.Rulesets.Edit
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider, OsuConfigManager config)
+ private void load(OsuConfigManager config)
{
autoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement);
@@ -109,6 +116,11 @@ namespace osu.Game.Rulesets.Edit
PlayfieldContentContainer = new Container
{
Name = "Content",
+ Padding = new MarginPadding
+ {
+ Left = TOOLBOX_CONTRACTED_SIZE_LEFT,
+ Right = TOOLBOX_CONTRACTED_SIZE_RIGHT,
+ },
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
@@ -131,7 +143,7 @@ namespace osu.Game.Rulesets.Edit
Colour = colourProvider.Background5,
RelativeSizeAxes = Axes.Both,
},
- new ExpandingToolboxContainer(60, 200)
+ LeftToolbox = new ExpandingToolboxContainer(TOOLBOX_CONTRACTED_SIZE_LEFT, 200)
{
Children = new Drawable[]
{
@@ -153,6 +165,28 @@ namespace osu.Game.Rulesets.Edit
},
}
},
+ new Container
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colourProvider.Background5,
+ RelativeSizeAxes = Axes.Both,
+ },
+ RightToolbox = new ExpandingToolboxContainer(TOOLBOX_CONTRACTED_SIZE_RIGHT, 250)
+ {
+ Child = new EditorToolboxGroup("inspector")
+ {
+ Child = new HitObjectInspector()
+ },
+ }
+ }
+ }
};
toolboxCollection.Items = CompositionTools
@@ -421,6 +455,9 @@ namespace osu.Game.Rulesets.Edit
[Cached]
public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
{
+ public const float TOOLBOX_CONTRACTED_SIZE_LEFT = 60;
+ public const float TOOLBOX_CONTRACTED_SIZE_RIGHT = 130;
+
public readonly Ruleset Ruleset;
protected HitObjectComposer(Ruleset ruleset)
diff --git a/osu.Game/Rulesets/Edit/HitObjectInspector.cs b/osu.Game/Rulesets/Edit/HitObjectInspector.cs
new file mode 100644
index 0000000000..977d00ede2
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/HitObjectInspector.cs
@@ -0,0 +1,146 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Threading;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Overlays;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Screens.Edit;
+
+namespace osu.Game.Rulesets.Edit
+{
+ internal partial class HitObjectInspector : CompositeDrawable
+ {
+ private OsuTextFlowContainer inspectorText = null!;
+
+ [Resolved]
+ protected EditorBeatmap EditorBeatmap { get; private set; } = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Y;
+ RelativeSizeAxes = Axes.X;
+
+ InternalChild = inspectorText = new OsuTextFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, _) => updateInspectorText();
+ EditorBeatmap.TransactionBegan += updateInspectorText;
+ EditorBeatmap.TransactionEnded += updateInspectorText;
+ updateInspectorText();
+ }
+
+ private ScheduledDelegate? rollingTextUpdate;
+
+ private void updateInspectorText()
+ {
+ inspectorText.Clear();
+ rollingTextUpdate?.Cancel();
+ rollingTextUpdate = null;
+
+ switch (EditorBeatmap.SelectedHitObjects.Count)
+ {
+ case 0:
+ addValue("No selection");
+ break;
+
+ case 1:
+ var selected = EditorBeatmap.SelectedHitObjects.Single();
+
+ addHeader("Type");
+ addValue($"{selected.GetType().ReadableName()}");
+
+ addHeader("Time");
+ addValue($"{selected.StartTime:#,0.##}ms");
+
+ switch (selected)
+ {
+ case IHasPosition pos:
+ addHeader("Position");
+ addValue($"x:{pos.X:#,0.##} y:{pos.Y:#,0.##}");
+ break;
+
+ case IHasXPosition x:
+ addHeader("Position");
+
+ addValue($"x:{x.X:#,0.##} ");
+ break;
+
+ case IHasYPosition y:
+ addHeader("Position");
+
+ addValue($"y:{y.Y:#,0.##}");
+ break;
+ }
+
+ if (selected is IHasDistance distance)
+ {
+ addHeader("Distance");
+ addValue($"{distance.Distance:#,0.##}px");
+ }
+
+ if (selected is IHasRepeats repeats)
+ {
+ addHeader("Repeats");
+ addValue($"{repeats.RepeatCount:#,0.##}");
+ }
+
+ if (selected is IHasDuration duration)
+ {
+ addHeader("End Time");
+ addValue($"{duration.EndTime:#,0.##}ms");
+ addHeader("Duration");
+ addValue($"{duration.Duration:#,0.##}ms");
+ }
+
+ // I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes.
+ // This is a good middle-ground for the time being.
+ rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250);
+ break;
+
+ default:
+ addHeader("Selected Objects");
+ addValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}");
+
+ addHeader("Start Time");
+ addValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms");
+
+ addHeader("End Time");
+ addValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms");
+ break;
+ }
+
+ void addHeader(string header) => inspectorText.AddParagraph($"{header}: ", s =>
+ {
+ s.Padding = new MarginPadding { Top = 2 };
+ s.Font = s.Font.With(size: 12);
+ s.Colour = colourProvider.Content2;
+ });
+
+ void addValue(string value) => inspectorText.AddParagraph(value, s =>
+ {
+ s.Font = s.Font.With(weight: FontWeight.SemiBold);
+ s.Colour = colourProvider.Content1;
+ });
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
index f810f51027..bdcb334738 100644
--- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Edit
HitObject = hitObject;
// adding the default hit sample should be the case regardless of the ruleset.
- HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
+ HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK, volume: 100));
RelativeSizeAxes = Axes.Both;
@@ -74,9 +74,10 @@ namespace osu.Game.Rulesets.Edit
/// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.
protected void BeginPlacement(bool commitStart = false)
{
- var nearestSampleControlPoint = beatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.SampleControlPoint?.DeepClone() as SampleControlPoint;
-
- HitObject.SampleControlPoint = nearestSampleControlPoint ?? new SampleControlPoint();
+ // Take the hitnormal sample of the last hit object
+ var lastHitNormal = beatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
+ if (lastHitNormal != null)
+ HitObject.Samples[0] = lastHitNormal;
placementHandler.BeginPlacement(HitObject);
if (commitStart)
diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
index 9e4469bf25..733610c040 100644
--- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs
+++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
@@ -24,5 +24,22 @@ namespace osu.Game.Rulesets.Mods
MaxValue = 2,
Precision = 0.01,
};
+
+ public override double ScoreMultiplier
+ {
+ get
+ {
+ // Round to the nearest multiple of 0.1.
+ double value = (int)(SpeedChange.Value * 10) / 10.0;
+
+ // Offset back to 0.
+ value -= 1;
+
+ // Each 0.1 multiple changes score multiplier by 0.02.
+ value /= 5;
+
+ return 1 + value;
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs
index 4425ece513..97789b7f5a 100644
--- a/osu.Game/Rulesets/Mods/ModFailCondition.cs
+++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs
@@ -20,11 +20,31 @@ namespace osu.Game.Rulesets.Mods
public virtual bool RestartOnFail => Restart.Value;
+ private Action? triggerFailureDelegate;
+
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
{
+ triggerFailureDelegate = healthProcessor.TriggerFailure;
healthProcessor.FailConditions += FailCondition;
}
+ ///
+ /// Immediately triggers a failure on the loaded .
+ ///
+ protected void TriggerFailure() => triggerFailureDelegate?.Invoke();
+
+ ///
+ /// Determines whether should trigger a failure. Called every time a
+ /// judgement is applied to .
+ ///
+ /// The loaded .
+ /// The latest .
+ /// Whether the fail condition has been met.
+ ///
+ /// This method should only be used to trigger failures based on .
+ /// Using outside values to evaluate failure may introduce event ordering discrepancies, use
+ /// an with instead.
+ ///
protected abstract bool FailCondition(HealthProcessor healthProcessor, JudgementResult result);
}
}
diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index f8c3a730f2..215fc877dc 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -82,8 +82,11 @@ namespace osu.Game.Rulesets.Mods
flashlight.RelativeSizeAxes = Axes.Both;
flashlight.Colour = Color4.Black;
+ // Flashlight mods should always draw above any other mod adding overlays.
+ flashlight.Depth = float.MinValue;
flashlight.Combo.BindTo(Combo);
+
drawableRuleset.Overlays.Add(flashlight);
}
diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs
index 7d858dca6f..06c7750035 100644
--- a/osu.Game/Rulesets/Mods/ModHalfTime.cs
+++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs
@@ -24,5 +24,19 @@ namespace osu.Game.Rulesets.Mods
MaxValue = 0.99,
Precision = 0.01,
};
+
+ public override double ScoreMultiplier
+ {
+ get
+ {
+ // Round to the nearest multiple of 0.1.
+ double value = (int)(SpeedChange.Value * 10) / 10.0;
+
+ // Offset back to 0.
+ value -= 1;
+
+ return 1 + value;
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 0c50f8341a..79fc778287 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -239,6 +239,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
OnNestedDrawableCreated?.Invoke(drawableNested);
drawableNested.OnNewResult += onNewResult;
+ drawableNested.OnRevertResult += onNestedRevertResult;
drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;
// This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation().
@@ -312,6 +313,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
foreach (var obj in nestedHitObjects)
{
obj.OnNewResult -= onNewResult;
+ obj.OnRevertResult -= onNestedRevertResult;
obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
@@ -355,13 +357,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (samples.Length <= 0)
return;
- if (HitObject.SampleControlPoint == null)
- {
- throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
- + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
- }
-
- Samples.Samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray();
+ Samples.Samples = samples.Cast().ToArray();
}
private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples();
@@ -376,6 +372,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
OnRevertResult?.Invoke(this, Result);
}
+ private void onNestedRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result);
+
private void onApplyCustomUpdateState(DrawableHitObject drawableHitObject, ArmedState state) => ApplyCustomUpdateState?.Invoke(drawableHitObject, state);
private void onDefaultsApplied(HitObject hitObject)
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index 25f538d211..a4cb976d50 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -16,7 +16,6 @@ using osu.Framework.Lists;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -77,8 +76,11 @@ namespace osu.Game.Rulesets.Objects
///
public virtual IList AuxiliarySamples => ImmutableList.Empty;
- public SampleControlPoint SampleControlPoint = SampleControlPoint.DEFAULT;
- public DifficultyControlPoint DifficultyControlPoint = DifficultyControlPoint.DEFAULT;
+ ///
+ /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
+ /// DO NOT USE THIS UNLESS 100% SURE.
+ ///
+ public double? LegacyBpmMultiplier { get; set; }
///
/// Whether this is in Kiai time.
@@ -105,25 +107,8 @@ 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();
- else if (ReferenceEquals(DifficultyControlPoint, DifficultyControlPoint.DEFAULT))
- DifficultyControlPoint = new DifficultyControlPoint();
-
- DifficultyControlPoint.Time = StartTime;
-
ApplyDefaultsToSelf(controlPointInfo, difficulty);
- // This is done here after ApplyDefaultsToSelf as we may require custom defaults to be applied to have an accurate end time.
- if (legacyInfo != null)
- SampleControlPoint = (SampleControlPoint)legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency).DeepClone();
- else if (ReferenceEquals(SampleControlPoint, SampleControlPoint.DEFAULT))
- SampleControlPoint = new SampleControlPoint();
-
- SampleControlPoint.Time = this.GetEndTime() + control_point_leniency;
-
nestedHitObjects.Clear();
CreateNestedHitObjects(cancellationToken);
@@ -164,9 +149,6 @@ namespace osu.Game.Rulesets.Objects
foreach (var nested in nestedHitObjects)
nested.StartTime += offset;
-
- DifficultyControlPoint.Time = time.NewValue;
- SampleControlPoint.Time = this.GetEndTime() + control_point_leniency;
}
}
@@ -222,6 +204,17 @@ namespace osu.Game.Rulesets.Objects
return slidingSamples;
}
+
+ ///
+ /// Create a SampleInfo based on the sample settings of the hit normal sample in .
+ ///
+ /// The name of the sample.
+ /// A populated .
+ protected HitSampleInfo GetSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL)
+ {
+ var hitnormalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
+ return hitnormalSample == null ? new HitSampleInfo(sampleName) : hitnormalSample.With(newName: sampleName);
+ }
}
public static class HitObjectExtensions
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 68ca6bc506..ba5de6c14b 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
if (split.Length > 10)
- readCustomSampleBanks(split[10], bankInfo);
+ readCustomSampleBanks(split[10], bankInfo, true);
// One node for each repeat + the start and end nodes
int nodes = repeatCount + 2;
@@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
return result;
}
- private void readCustomSampleBanks(string str, SampleBankInfo bankInfo)
+ private void readCustomSampleBanks(string str, SampleBankInfo bankInfo, bool banksOnly = false)
{
if (string.IsNullOrEmpty(str))
return;
@@ -202,6 +202,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
bankInfo.BankForNormal = stringBank;
bankInfo.BankForAdditions = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank;
+ if (banksOnly) return;
+
if (split.Length > 2)
bankInfo.CustomSampleBank = Parsing.ParseInt(split[2]);
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
index bd2713a7d1..7ddd372dc9 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
@@ -6,13 +6,14 @@
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
using Newtonsoft.Json;
+using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Objects.Legacy
{
- internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset
+ internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset, IHasSliderVelocity
{
///
/// Scoring distance with a speed-adjusted beat length of 1 second.
@@ -40,13 +41,21 @@ namespace osu.Game.Rulesets.Objects.Legacy
public double Velocity = 1;
+ public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1);
+
+ public double SliderVelocity
+ {
+ get => SliderVelocityBindable.Value;
+ set => SliderVelocityBindable.Value = value;
+ }
+
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
+ double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
}
diff --git a/osu.Game/Rulesets/Objects/Types/IHasGenerateTicks.cs b/osu.Game/Rulesets/Objects/Types/IHasGenerateTicks.cs
new file mode 100644
index 0000000000..3ac8b8a086
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/Types/IHasGenerateTicks.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Objects.Types
+{
+ ///
+ /// A type of which explicitly specifies whether it should generate ticks.
+ ///
+ public interface IHasGenerateTicks
+ {
+ ///
+ /// Whether or not slider ticks should be generated by this object.
+ /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991).
+ ///
+ public bool GenerateTicks { get; set; }
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs b/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs
new file mode 100644
index 0000000000..c0ac5036ee
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+
+namespace osu.Game.Rulesets.Objects.Types;
+
+///
+/// A HitObject that has a slider velocity multiplier.
+///
+public interface IHasSliderVelocity
+{
+ ///
+ /// The slider velocity multiplier.
+ ///
+ double SliderVelocity { get; set; }
+
+ BindableNumber SliderVelocityBindable { get; }
+}
diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
index b70ddd5e24..3e0b6433c2 100644
--- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
@@ -31,6 +31,15 @@ namespace osu.Game.Rulesets.Scoring
///
public bool HasFailed { get; private set; }
+ ///
+ /// Immediately triggers a failure for this HealthProcessor.
+ ///
+ public void TriggerFailure()
+ {
+ if (Failed?.Invoke() != false)
+ HasFailed = true;
+ }
+
protected override void ApplyResultInternal(JudgementResult result)
{
result.HealthAtJudgement = Health.Value;
@@ -42,10 +51,7 @@ namespace osu.Game.Rulesets.Scoring
Health.Value += GetHealthIncreaseFor(result);
if (meetsAnyFailCondition(result))
- {
- if (Failed?.Invoke() != false)
- HasFailed = true;
- }
+ TriggerFailure();
}
protected override void RevertResultInternal(JudgementResult result)
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index 76d84000f1..e34289c968 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -206,27 +206,11 @@ namespace osu.Game.Rulesets.UI
this.parent = parent;
}
- // When the debugger is attached, exceptions are expensive.
- // Manually work around this by caching failed lookups and falling back straight to parent.
- private readonly HashSet<(string, string)> failedLookups = new HashSet<(string, string)>();
+ public override IShader? GetCachedShader(string vertex, string fragment) => base.GetCachedShader(vertex, fragment) ?? parent.GetCachedShader(vertex, fragment);
- public override IShader Load(string vertex, string fragment)
- {
- if (!failedLookups.Contains((vertex, fragment)))
- {
- try
- {
- return base.Load(vertex, fragment);
- }
- catch
- {
- // Shader lookup is very non-standard. Rather than returning null on missing shaders, exceptions are thrown.
- failedLookups.Add((vertex, fragment));
- }
- }
+ public override IShaderPart? GetCachedShaderPart(string name) => base.GetCachedShaderPart(name) ?? parent.GetCachedShaderPart(name);
- return parent.Load(vertex, fragment);
- }
+ public override byte[]? GetRawData(string fileName) => base.GetRawData(fileName) ?? parent.GetRawData(fileName);
}
}
}
diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs
index e1c03e49e3..d4510a4519 100644
--- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs
+++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs
@@ -52,7 +52,6 @@ namespace osu.Game.Rulesets.UI
return;
var samples = nextObject.Samples
- .Select(s => nextObject.SampleControlPoint.ApplyTo(s))
.Cast()
.ToArray();
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index b1c3b78e67..6016a53918 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -293,10 +293,10 @@ namespace osu.Game.Rulesets.UI
{
// prepare sample pools ahead of time so we're not initialising at runtime.
foreach (var sample in hitObject.Samples)
- prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample));
+ prepareSamplePool(sample);
foreach (var sample in hitObject.AuxiliarySamples)
- prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample));
+ prepareSamplePool(sample);
foreach (var nestedObject in hitObject.NestedHitObjects)
preloadSamples(nestedObject);
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs
index 0f702e1c49..c2a3f12efd 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs
@@ -7,14 +7,15 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
-using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
+using osu.Game.Localisation;
using osuTK;
using osuTK.Graphics;
+using Key = osuTK.Input.Key;
namespace osu.Game.Screens.Edit.Compose.Components
{
@@ -26,6 +27,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private SpriteIcon icon;
+ private const float snap_step = 15;
+
private readonly Bindable cumulativeRotation = new Bindable();
[Resolved]
@@ -50,18 +53,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
});
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
- cumulativeRotation.BindValueChanged(_ => updateTooltipText(), true);
- }
-
protected override void UpdateHoverState()
{
base.UpdateHoverState();
icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint);
}
+ private float rawCumulativeRotation;
+
protected override bool OnDragStart(DragStartEvent e)
{
bool handle = base.OnDragStart(e);
@@ -74,21 +73,36 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.OnDrag(e);
- float instantaneousAngle = convertDragEventToAngleOfRotation(e);
- cumulativeRotation.Value += instantaneousAngle;
+ rawCumulativeRotation += convertDragEventToAngleOfRotation(e);
- if (cumulativeRotation.Value < -180)
- cumulativeRotation.Value += 360;
- else if (cumulativeRotation.Value > 180)
- cumulativeRotation.Value -= 360;
+ applyRotation(shouldSnap: e.ShiftPressed);
+ }
- HandleRotate?.Invoke(instantaneousAngle);
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight))
+ {
+ applyRotation(shouldSnap: true);
+ return true;
+ }
+
+ return base.OnKeyDown(e);
+ }
+
+ protected override void OnKeyUp(KeyUpEvent e)
+ {
+ base.OnKeyUp(e);
+
+ if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight))
+ applyRotation(shouldSnap: false);
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
cumulativeRotation.Value = null;
+ rawCumulativeRotation = 0;
+ TooltipText = default;
}
private float convertDragEventToAngleOfRotation(DragEvent e)
@@ -100,9 +114,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
return (endAngle - startAngle) * 180 / MathF.PI;
}
- private void updateTooltipText()
+ private void applyRotation(bool shouldSnap)
{
- TooltipText = cumulativeRotation.Value?.ToLocalisableString("0.0°") ?? default;
+ float oldRotation = cumulativeRotation.Value ?? 0;
+
+ float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation);
+ newRotation = (newRotation - 180) % 360 + 180;
+
+ cumulativeRotation.Value = newRotation;
+
+ HandleRotate?.Invoke(newRotation - oldRotation);
+ TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation);
}
+
+ private float snap(float value, float step) => MathF.Round(value / step) * step;
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
index d3cdd461ea..4741b75641 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
@@ -13,12 +13,14 @@ 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;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Timing;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
@@ -29,13 +31,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly BindableNumber speedMultiplier;
public DifficultyPointPiece(HitObject hitObject)
- : base(hitObject.DifficultyControlPoint)
{
HitObject = hitObject;
- speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy();
+ speedMultiplier = (hitObject as IHasSliderVelocity)?.SliderVelocityBindable.GetBoundCopy();
}
+ protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -78,7 +81,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Spacing = new Vector2(0, 15),
Children = new Drawable[]
{
- sliderVelocitySlider = new IndeterminateSliderWithTextBoxInput("Velocity", new DifficultyControlPoint().SliderVelocityBindable)
+ sliderVelocitySlider = new IndeterminateSliderWithTextBoxInput("Velocity", new BindableDouble(1)
+ {
+ Precision = 0.01,
+ MinValue = 0.1,
+ MaxValue = 10
+ })
{
KeyboardStep = 0.1f
},
@@ -94,11 +102,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
// if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
// if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
- var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();
- var relevantControlPoints = relevantObjects.Select(h => h.DifficultyControlPoint).ToArray();
+ var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).Where(o => o is IHasSliderVelocity).ToArray();
// even if there are multiple objects selected, we can still display a value if they all have the same value.
- var selectedPointBindable = relevantControlPoints.Select(point => point.SliderVelocity).Distinct().Count() == 1 ? relevantControlPoints.First().SliderVelocityBindable : null;
+ var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocity).Distinct().Count() == 1 ? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable : null;
if (selectedPointBindable != null)
{
@@ -117,7 +124,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
foreach (var h in relevantObjects)
{
- h.DifficultyControlPoint.SliderVelocity = val.NewValue.Value;
+ ((IHasSliderVelocity)h).SliderVelocity = val.NewValue.Value;
beatmap.Update(h);
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs
index 5b0a5729c8..4b357d3a62 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs
@@ -7,7 +7,6 @@ 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;
@@ -16,21 +15,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public partial 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);
+ Color4 colour = GetRepresentingColour(colours);
InternalChildren = new Drawable[]
{
@@ -61,5 +53,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
},
};
}
+
+ protected virtual Color4 GetRepresentingColour(OsuColour colours)
+ {
+ return colours.Yellow;
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
index 314137a565..b02cfb505e 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
@@ -12,11 +12,13 @@ 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.Audio;
+using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Timing;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
@@ -24,22 +26,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public readonly HitObject HitObject;
- private readonly Bindable bank;
- private readonly BindableNumber volume;
+ private readonly BindableList samplesBindable;
public SamplePointPiece(HitObject hitObject)
- : base(hitObject.SampleControlPoint)
{
HitObject = hitObject;
- volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy();
- bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy();
+ samplesBindable = hitObject.SamplesBindable.GetBoundCopy();
}
+ protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Pink;
+
[BackgroundDependencyLoader]
private void load()
{
- volume.BindValueChanged(_ => updateText());
- bank.BindValueChanged(_ => updateText(), true);
+ samplesBindable.BindCollectionChanged((_, _) => updateText(), true);
}
protected override bool OnClick(ClickEvent e)
@@ -50,7 +50,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateText()
{
- Label.Text = $"{bank.Value} {volume.Value}";
+ Label.Text = $"{GetBankValue(samplesBindable)} {GetVolumeValue(samplesBindable)}";
+ }
+
+ public static string? GetBankValue(IEnumerable samples)
+ {
+ return samples.FirstOrDefault()?.Bank;
+ }
+
+ public static int GetVolumeValue(ICollection samples)
+ {
+ return samples.Count == 0 ? 0 : samples.Max(o => o.Volume);
}
public Popover GetPopover() => new SampleEditPopover(HitObject);
@@ -89,7 +99,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
Label = "Bank Name",
},
- volume = new IndeterminateSliderWithTextBoxInput("Volume", new SampleControlPoint().SampleVolumeBindable)
+ volume = new IndeterminateSliderWithTextBoxInput("Volume", new BindableInt(100)
+ {
+ MinValue = 0,
+ MaxValue = 100,
+ })
}
}
};
@@ -100,14 +114,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
// if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
// if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();
- var relevantControlPoints = relevantObjects.Select(h => h.SampleControlPoint).ToArray();
+ var relevantSamples = relevantObjects.Select(h => h.Samples).ToArray();
// even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value.
- string? commonBank = getCommonBank(relevantControlPoints);
+ string? commonBank = getCommonBank(relevantSamples);
if (!string.IsNullOrEmpty(commonBank))
bank.Current.Value = commonBank;
- int? commonVolume = getCommonVolume(relevantControlPoints);
+ int? commonVolume = getCommonVolume(relevantSamples);
if (commonVolume != null)
volume.Current.Value = commonVolume.Value;
@@ -117,9 +131,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
updateBankFor(relevantObjects, val.NewValue);
updateBankPlaceholderText(relevantObjects);
});
- // on commit, ensure that the value is correct by sourcing it from the objects' control points again.
+ // on commit, ensure that the value is correct by sourcing it from the objects' samples again.
// this ensures that committing empty text causes a revert to the previous value.
- bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(relevantControlPoints);
+ bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(relevantSamples);
volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue));
}
@@ -130,8 +144,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume));
}
- private static string? getCommonBank(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleBank).Distinct().Count() == 1 ? relevantControlPoints.First().SampleBank : null;
- private static int? getCommonVolume(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleVolume).Distinct().Count() == 1 ? relevantControlPoints.First().SampleVolume : null;
+ private static string? getCommonBank(IList[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null;
+ private static int? getCommonVolume(IList[] relevantSamples) => relevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(relevantSamples.First()) : null;
private void updateBankFor(IEnumerable objects, string? newBank)
{
@@ -142,7 +156,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
foreach (var h in objects)
{
- h.SampleControlPoint.SampleBank = newBank;
+ for (int i = 0; i < h.Samples.Count; i++)
+ {
+ h.Samples[i] = h.Samples[i].With(newBank: newBank);
+ }
+
beatmap.Update(h);
}
@@ -151,7 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateBankPlaceholderText(IEnumerable objects)
{
- string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray());
+ string? commonBank = getCommonBank(objects.Select(h => h.Samples).ToArray());
bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty;
}
@@ -164,7 +182,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
foreach (var h in objects)
{
- h.SampleControlPoint.SampleVolume = newVolume.Value;
+ for (int i = 0; i < h.Samples.Count; i++)
+ {
+ h.Samples[i] = h.Samples[i].With(newVolume: newVolume.Value);
+ }
+
beatmap.Update(h);
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index 4e5087c004..ea063e9216 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -15,7 +15,6 @@ 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.Overlays;
@@ -102,6 +101,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
},
}
},
+ new SamplePointPiece(Item)
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.TopCentre
+ },
});
if (item is IHasDuration)
@@ -111,6 +115,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
OnDragHandled = e => OnDragHandled?.Invoke(e)
});
}
+
+ if (item is IHasSliderVelocity)
+ {
+ AddInternal(new DifficultyPointPiece(Item)
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.BottomCentre
+ });
+ }
}
protected override void LoadComplete()
@@ -187,12 +200,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
colouredComponents.Colour = OsuColour.ForegroundTextColourFor(averageColour);
}
- private SamplePointPiece? sampleOverrideDisplay;
- private DifficultyPointPiece? difficultyOverrideDisplay;
-
- private DifficultyControlPoint difficultyControlPoint = null!;
- private SampleControlPoint sampleControlPoint = null!;
-
protected override void Update()
{
base.Update();
@@ -208,36 +215,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (Item is IHasRepeats repeats)
updateRepeats(repeats);
}
-
- if (!ReferenceEquals(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 (!ReferenceEquals(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)
@@ -395,17 +372,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
case IHasRepeats repeatHitObject:
double proposedDuration = time - hitObject.StartTime;
- if (e.CurrentState.Keyboard.ShiftPressed)
+ if (e.CurrentState.Keyboard.ShiftPressed && hitObject is IHasSliderVelocity hasSliderVelocity)
{
- if (ReferenceEquals(hitObject.DifficultyControlPoint, DifficultyControlPoint.DEFAULT))
- hitObject.DifficultyControlPoint = new DifficultyControlPoint();
+ double newVelocity = hasSliderVelocity.SliderVelocity * (repeatHitObject.Duration / proposedDuration);
- double newVelocity = hitObject.DifficultyControlPoint.SliderVelocity * (repeatHitObject.Duration / proposedDuration);
-
- if (Precision.AlmostEquals(newVelocity, hitObject.DifficultyControlPoint.SliderVelocity))
+ if (Precision.AlmostEquals(newVelocity, hasSliderVelocity.SliderVelocity))
return;
- hitObject.DifficultyControlPoint.SliderVelocity = newVelocity;
+ hasSliderVelocity.SliderVelocity = newVelocity;
beatmap.Update(hitObject);
}
else
@@ -414,7 +388,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
double lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1);
int proposedCount = Math.Max(0, (int)Math.Round(proposedDuration / lengthOfOneRepeat) - 1);
- if (proposedCount == repeatHitObject.RepeatCount)
+ if (proposedCount == repeatHitObject.RepeatCount || lengthOfOneRepeat == 0)
return;
repeatHitObject.RepeatCount = proposedCount;
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index c9a31a0ad6..b8fa7f6579 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -205,7 +205,10 @@ namespace osu.Game.Screens.Edit
// this is a bit haphazard, but guards against setting the lease Beatmap bindable if
// the editor has already been exited.
if (!ValidForPush)
+ {
+ beatmapManager.Delete(loadableBeatmap.BeatmapSetInfo);
return;
+ }
}
try
diff --git a/osu.Game/Screens/Play/ArgonKeyCounter.cs b/osu.Game/Screens/Play/ArgonKeyCounter.cs
new file mode 100644
index 0000000000..6818b30823
--- /dev/null
+++ b/osu.Game/Screens/Play/ArgonKeyCounter.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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Screens.Play.HUD;
+using osuTK;
+
+namespace osu.Game.Screens.Play
+{
+ public partial class ArgonKeyCounter : KeyCounter
+ {
+ private Circle inputIndicator = null!;
+ private OsuSpriteText countText = null!;
+
+ // These values were taken from Figma
+ private const float line_height = 3;
+ private const float name_font_size = 10;
+ private const float count_font_size = 14;
+
+ // Make things look bigger without using Scale
+ private const float scale_factor = 1.5f;
+
+ public ArgonKeyCounter(InputTrigger trigger)
+ : base(trigger)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Children = new Drawable[]
+ {
+ inputIndicator = new Circle
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ Height = line_height * scale_factor,
+ Alpha = 0.5f
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Position = new Vector2(0, -13) * scale_factor,
+ Font = OsuFont.Torus.With(size: name_font_size * scale_factor, weight: FontWeight.Bold),
+ Colour = colours.Blue0,
+ Text = Trigger.Name
+ },
+ countText = new OsuSpriteText
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Font = OsuFont.Torus.With(size: count_font_size * scale_factor, weight: FontWeight.Bold),
+ },
+ };
+
+ // Values from Figma didn't match visually
+ // So these were just eyeballed
+ Height = 30 * scale_factor;
+ Width = 35 * scale_factor;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ IsActive.BindValueChanged(e => inputIndicator.Alpha = e.NewValue ? 1 : 0.5f, true);
+ CountPresses.BindValueChanged(e => countText.Text = e.NewValue.ToString(@"#,0"), true);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs
new file mode 100644
index 0000000000..984c2a7287
--- /dev/null
+++ b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Screens.Play.HUD;
+using osuTK;
+
+namespace osu.Game.Screens.Play
+{
+ public partial class ArgonKeyCounterDisplay : KeyCounterDisplay
+ {
+ private const int duration = 100;
+
+ protected override FillFlowContainer KeyFlow { get; }
+
+ public ArgonKeyCounterDisplay()
+ {
+ InternalChild = KeyFlow = new FillFlowContainer
+ {
+ Direction = FillDirection.Horizontal,
+ AutoSizeAxes = Axes.Both,
+ Alpha = 0,
+ Spacing = new Vector2(2),
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ Size = KeyFlow.Size;
+ }
+
+ protected override KeyCounter CreateCounter(InputTrigger trigger) => new ArgonKeyCounter(trigger);
+
+ protected override void UpdateVisibility()
+ => KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs
index 69a3e53dfc..f7ac72035f 100644
--- a/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Screens.Play.HUD
{
new OsuSpriteText
{
- Text = Name,
+ Text = Trigger.Name,
Font = OsuFont.Numeric.With(size: 12),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs
index 14d7f56093..e459574243 100644
--- a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs
@@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK.Graphics;
@@ -13,13 +13,11 @@ namespace osu.Game.Screens.Play.HUD
private const int duration = 100;
private const double key_fade_time = 80;
- private readonly FillFlowContainer keyFlow;
-
- public override IEnumerable Counters => keyFlow;
+ protected override FillFlowContainer KeyFlow { get; }
public DefaultKeyCounterDisplay()
{
- InternalChild = keyFlow = new FillFlowContainer
+ InternalChild = KeyFlow = new FillFlowContainer
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
@@ -33,20 +31,19 @@ namespace osu.Game.Screens.Play.HUD
// Don't use autosize as it will shrink to zero when KeyFlow is hidden.
// In turn this can cause the display to be masked off screen and never become visible again.
- Size = keyFlow.Size;
+ Size = KeyFlow.Size;
}
- public override void Add(InputTrigger trigger) =>
- keyFlow.Add(new DefaultKeyCounter(trigger)
- {
- FadeTime = key_fade_time,
- KeyDownTextColor = KeyDownTextColor,
- KeyUpTextColor = KeyUpTextColor,
- });
+ protected override KeyCounter CreateCounter(InputTrigger trigger) => new DefaultKeyCounter(trigger)
+ {
+ FadeTime = key_fade_time,
+ KeyDownTextColor = KeyDownTextColor,
+ KeyUpTextColor = KeyUpTextColor,
+ };
protected override void UpdateVisibility() =>
// Isolate changing visibility of the key counters from fading this component.
- keyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
+ KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
private Color4 keyDownTextColor = Color4.DarkGray;
@@ -58,7 +55,7 @@ namespace osu.Game.Screens.Play.HUD
if (value != keyDownTextColor)
{
keyDownTextColor = value;
- foreach (var child in keyFlow)
+ foreach (var child in KeyFlow.Cast())
child.KeyDownTextColor = value;
}
}
@@ -74,7 +71,7 @@ namespace osu.Game.Screens.Play.HUD
if (value != keyUpTextColor)
{
keyUpTextColor = value;
- foreach (var child in keyFlow)
+ foreach (var child in KeyFlow.Cast())
child.KeyUpTextColor = value;
}
}
diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs
index 2a4ab1993a..7cdd6b025f 100644
--- a/osu.Game/Screens/Play/HUD/KeyCounter.cs
+++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs
@@ -54,8 +54,6 @@ namespace osu.Game.Screens.Play.HUD
Trigger.OnActivate += Activate;
Trigger.OnDeactivate += Deactivate;
-
- Name = trigger.Name;
}
private void increment()
diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs
index 49c0da6793..05427d3a32 100644
--- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs
@@ -29,7 +29,9 @@ namespace osu.Game.Screens.Play.HUD
///
/// The s contained in this .
///
- public abstract IEnumerable Counters { get; }
+ public IEnumerable Counters => KeyFlow;
+
+ protected abstract FillFlowContainer KeyFlow { get; }
///
/// Whether the actions reported by all s within this should be counted.
@@ -53,13 +55,22 @@ namespace osu.Game.Screens.Play.HUD
///
/// Add a to this display.
///
- public abstract void Add(InputTrigger trigger);
+ public void Add(InputTrigger trigger)
+ {
+ var keyCounter = CreateCounter(trigger);
+
+ KeyFlow.Add(keyCounter);
+
+ IsCounting.BindTo(keyCounter.IsCounting);
+ }
///
/// Add a range of to this display.
///
public void AddRange(IEnumerable triggers) => triggers.ForEach(Add);
+ protected abstract KeyCounter CreateCounter(InputTrigger trigger);
+
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 68d3247275..6ba9843f7b 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Screens.Select
public float BleedBottom { get; set; }
///
- /// Triggered when the loaded change and are completely loaded.
+ /// Triggered when finish loading, or are subsequently changed.
///
public Action? BeatmapSetsChanged;
@@ -353,6 +353,8 @@ namespace osu.Game.Screens.Select
if (!Scroll.UserScrolling)
ScrollToSelected(true);
+
+ BeatmapSetsChanged?.Invoke();
});
public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs
index f065926eb7..0c3de5848b 100644
--- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs
+++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs
@@ -58,7 +58,7 @@ namespace osu.Game.Screens.Select.Carousel
item = value;
- if (IsLoaded)
+ if (IsLoaded && !IsDisposed)
UpdateItem();
}
}
@@ -165,5 +165,13 @@ namespace osu.Game.Screens.Select.Carousel
Item.State.Value = CarouselItemState.Selected;
return true;
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ // This is important to clean up event subscriptions.
+ Item = null;
+ }
}
}
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index c5e914b461..4d6a5398c5 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -861,11 +862,9 @@ namespace osu.Game.Screens.Select
private void updateVisibleBeatmapCount()
{
- FilterControl.InformationalText = Carousel.CountDisplayed == 1
- // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918
- // but also in this case we want support for formatting a number within a string).
- ? $"{Carousel.CountDisplayed:#,0} matching beatmap"
- : $"{Carousel.CountDisplayed:#,0} matching beatmaps";
+ // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918
+ // but also in this case we want support for formatting a number within a string).
+ FilterControl.InformationalText = $"{"match".ToQuantity(Carousel.CountDisplayed, "#,0")}";
}
private bool boundLocalBindables;
diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs
index 2c16a67cac..6523039a3f 100644
--- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs
+++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs
@@ -84,8 +84,8 @@ namespace osu.Game.Skinning.Components
private void updateBeatmapContent(WorkingBeatmap workingBeatmap)
{
- valueDictionary[BeatmapAttribute.Title] = workingBeatmap.BeatmapInfo.Metadata.Title;
- valueDictionary[BeatmapAttribute.Artist] = workingBeatmap.BeatmapInfo.Metadata.Artist;
+ valueDictionary[BeatmapAttribute.Title] = new RomanisableString(workingBeatmap.BeatmapInfo.Metadata.TitleUnicode, workingBeatmap.BeatmapInfo.Metadata.Title);
+ valueDictionary[BeatmapAttribute.Artist] = new RomanisableString(workingBeatmap.BeatmapInfo.Metadata.ArtistUnicode, workingBeatmap.BeatmapInfo.Metadata.Artist);
valueDictionary[BeatmapAttribute.DifficultyName] = workingBeatmap.BeatmapInfo.DifficultyName;
valueDictionary[BeatmapAttribute.Creator] = workingBeatmap.BeatmapInfo.Metadata.Author.Username;
valueDictionary[BeatmapAttribute.Length] = TimeSpan.FromMilliseconds(workingBeatmap.BeatmapInfo.Length).ToFormattedDuration();
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
index e598c79b08..be77c9a98e 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Storyboards.Drawables
Loop = animation.LoopType == AnimationLoopType.LoopForever;
LifetimeStart = animation.StartTime;
- LifetimeEnd = animation.EndTime;
+ LifetimeEnd = animation.EndTimeForDisplay;
}
[Resolved]
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs
index f9b09ed57c..400d33481c 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Storyboards.Drawables
Position = sprite.InitialPosition;
LifetimeStart = sprite.StartTime;
- LifetimeEnd = sprite.EndTime;
+ LifetimeEnd = sprite.EndTimeForDisplay;
}
[Resolved]
diff --git a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs
index c8daeb3b3d..9eed139ad4 100644
--- a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs
+++ b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs
@@ -12,9 +12,17 @@ namespace osu.Game.Storyboards
{
///
/// The time at which the ends.
+ /// This is consumed to extend the length of a storyboard to ensure all visuals are played to completion.
///
double EndTime { get; }
+ ///
+ /// The time this element displays until.
+ /// This is used for lifetime purposes, and includes long playing animations which don't necessarily extend
+ /// a storyboard's play time.
+ ///
+ double EndTimeForDisplay { get; }
+
///
/// The duration of the StoryboardElement.
///
diff --git a/osu.Game/Storyboards/StoryboardAnimation.cs b/osu.Game/Storyboards/StoryboardAnimation.cs
index 16deac8e9e..1a4b6bb923 100644
--- a/osu.Game/Storyboards/StoryboardAnimation.cs
+++ b/osu.Game/Storyboards/StoryboardAnimation.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osuTK;
using osu.Framework.Graphics;
using osu.Game.Storyboards.Drawables;
diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs
index 5b7b194be7..982185d51b 100644
--- a/osu.Game/Storyboards/StoryboardSprite.cs
+++ b/osu.Game/Storyboards/StoryboardSprite.cs
@@ -1,12 +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 disable
-
using System;
using System.Collections.Generic;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Game.Storyboards.Drawables;
using osuTK;
@@ -84,6 +81,19 @@ namespace osu.Game.Storyboards
}
}
+ public double EndTimeForDisplay
+ {
+ get
+ {
+ double latestEndTime = TimelineGroup.EndTime;
+
+ foreach (var l in loops)
+ latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
+
+ return latestEndTime;
+ }
+ }
+
public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands);
private delegate void DrawablePropertyInitializer(Drawable drawable, T value);
@@ -114,7 +124,7 @@ namespace osu.Game.Storyboards
public virtual Drawable CreateDrawable()
=> new DrawableStoryboardSprite(this);
- public void ApplyTransforms(Drawable drawable, IEnumerable> triggeredGroups = null)
+ public void ApplyTransforms(Drawable drawable, IEnumerable>? triggeredGroups = null)
{
// For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity.
// To achieve this, commands are "generated" as pairs of (command, initFunc, transformFunc) and batched into a contiguous list
@@ -156,7 +166,7 @@ namespace osu.Game.Storyboards
foreach (var command in commands)
{
- DrawablePropertyInitializer initFunc = null;
+ DrawablePropertyInitializer? initFunc = null;
if (!initialized)
{
@@ -169,7 +179,7 @@ namespace osu.Game.Storyboards
}
}
- private IEnumerable.TypedCommand> getCommands(CommandTimelineSelector timelineSelector, IEnumerable> triggeredGroups)
+ private IEnumerable.TypedCommand> getCommands(CommandTimelineSelector timelineSelector, IEnumerable>? triggeredGroups)
{
var commands = TimelineGroup.GetCommands(timelineSelector);
foreach (var loop in loops)
@@ -198,11 +208,11 @@ namespace osu.Game.Storyboards
{
public double StartTime => command.StartTime;
- private readonly DrawablePropertyInitializer initializeProperty;
+ private readonly DrawablePropertyInitializer? initializeProperty;
private readonly DrawableTransformer transform;
private readonly CommandTimeline.TypedCommand command;
- public GeneratedCommand([NotNull] CommandTimeline.TypedCommand command, [CanBeNull] DrawablePropertyInitializer initializeProperty, [NotNull] DrawableTransformer transform)
+ public GeneratedCommand(CommandTimeline.TypedCommand command, DrawablePropertyInitializer? initializeProperty, DrawableTransformer transform)
{
this.command = command;
this.initializeProperty = initializeProperty;
diff --git a/osu.Game/Tests/Visual/EditorSavingTestScene.cs b/osu.Game/Tests/Visual/EditorSavingTestScene.cs
index cd9e9e1d52..78188d7cf7 100644
--- a/osu.Game/Tests/Visual/EditorSavingTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorSavingTestScene.cs
@@ -3,9 +3,12 @@
#nullable disable
+using System;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Input;
using osu.Framework.Testing;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
@@ -24,18 +27,27 @@ namespace osu.Game.Tests.Visual
protected EditorBeatmap EditorBeatmap => (EditorBeatmap)Editor.Dependencies.Get(typeof(EditorBeatmap));
+ [CanBeNull]
+ protected Func CreateInitialBeatmap { get; set; }
+
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
- AddStep("set default beatmap", () => Game.Beatmap.SetDefault());
+ if (CreateInitialBeatmap == null)
+ AddStep("set default beatmap", () => Game.Beatmap.SetDefault());
+ else
+ {
+ AddStep("set test beatmap", () => Game.Beatmap.Value = CreateInitialBeatmap?.Invoke());
+ }
PushAndConfirm(() => new EditorLoader());
AddUntilStep("wait for editor load", () => Editor?.IsLoaded == true);
- AddUntilStep("wait for metadata screen load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
+ if (CreateInitialBeatmap == null)
+ AddUntilStep("wait for metadata screen load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
// We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten.
@@ -50,6 +62,14 @@ namespace osu.Game.Tests.Visual
protected void ReloadEditorToSameBeatmap()
{
+ Guid beatmapSetGuid = Guid.Empty;
+ Guid beatmapGuid = Guid.Empty;
+
+ AddStep("Store beatmap GUIDs", () =>
+ {
+ beatmapSetGuid = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID;
+ beatmapGuid = EditorBeatmap.BeatmapInfo.ID;
+ });
AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@@ -59,7 +79,8 @@ namespace osu.Game.Tests.Visual
PushAndConfirm(() => songSelect = new PlaySongSelect());
AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded);
- AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault);
+ AddStep("Present same beatmap", () => Game.PresentBeatmap(Game.BeatmapManager.QueryBeatmapSet(set => set.ID == beatmapSetGuid)!.Value, beatmap => beatmap.ID == beatmapGuid));
+ AddUntilStep("Wait for beatmap selected", () => Game.Beatmap.Value.BeatmapInfo.ID == beatmapGuid);
AddStep("Open options", () => InputManager.Key(Key.F3));
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index b9c6c1df9d..085f78b27b 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,8 +36,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 083d8192ea..127994c670 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -16,6 +16,6 @@
iossimulator-x64
-
+