diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index 6704fd82e9..fb8af9bdb6 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
- if (lookup is SkinComponentsContainerLookup componentLookup)
+ if (lookup is SkinComponentsContainerLookup containerLookup)
{
- switch (componentLookup.Target)
+ switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
var components = base.GetDrawableComponent(lookup) as Container;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 264a1bd5ec..25d0573a82 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -69,8 +69,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
private double? releaseTime;
- public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
-
public DrawableHoldNote()
: this(null)
{
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index bf477277c6..20ea962994 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -15,13 +15,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public partial class DrawableHoldNoteTail : DrawableNote
{
- ///
- /// Lenience of release hit windows. This is to make cases where the hold note release
- /// is timed alongside presses of other hit objects less awkward.
- /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
- ///
- private const double release_window_lenience = 1.5;
-
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
@@ -40,14 +33,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public void UpdateResult() => base.UpdateResult(true);
- public override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
-
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);
// Factor in the release lenience
- timeOffset /= release_window_lenience;
+ timeOffset /= TailNote.RELEASE_WINDOW_LENIENCE;
if (!userTriggered)
{
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index 22fab15c1b..c367886efe 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -81,6 +81,8 @@ namespace osu.Game.Rulesets.Mania.Objects
///
public TailNote Tail { get; private set; }
+ public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
+
///
/// The time between ticks of this hold.
///
diff --git a/osu.Game.Rulesets.Mania/Objects/TailNote.cs b/osu.Game.Rulesets.Mania/Objects/TailNote.cs
index cda8e2fa31..d6dc25079a 100644
--- a/osu.Game.Rulesets.Mania/Objects/TailNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/TailNote.cs
@@ -10,6 +10,15 @@ namespace osu.Game.Rulesets.Mania.Objects
{
public class TailNote : Note
{
+ ///
+ /// Lenience of release hit windows. This is to make cases where the hold note release
+ /// is timed alongside presses of other hit objects less awkward.
+ /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
+ ///
+ public const double RELEASE_WINDOW_LENIENCE = 1.5;
+
public override Judgement CreateJudgement() => new ManiaJudgement();
+
+ public override double MaximumJudgementOffset => base.MaximumJudgementOffset * RELEASE_WINDOW_LENIENCE;
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutopilot.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutopilot.cs
new file mode 100644
index 0000000000..37b31d1d1a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutopilot.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Osu.Mods;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public partial class TestSceneOsuModAutopilot : OsuModTestScene
+ {
+ [Test]
+ public void TestInstantResume()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new OsuModAutopilot(),
+ PassCondition = () => true,
+ Autoplay = false,
+ });
+
+ AddUntilStep("wait for gameplay start", () => Player.LocalUserPlaying.Value);
+ AddStep("press pause", () => InputManager.PressKey(Key.Escape));
+ AddUntilStep("wait until paused", () => Player.GameplayClockContainer.IsPaused.Value);
+ AddStep("release pause", () => InputManager.ReleaseKey(Key.Escape));
+ AddStep("press resume", () => InputManager.PressKey(Key.Escape));
+ AddUntilStep("wait for resume", () => !Player.IsResuming);
+ AddAssert("resumed", () => !Player.GameplayClockContainer.IsPaused.Value);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 6772cfe0be..9eb0a46bfb 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -60,6 +60,8 @@ namespace osu.Game.Rulesets.Osu.Mods
// Generate the replay frames the cursor should follow
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast().ToList();
+
+ drawableRuleset.UseResumeOverlay = false;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
index b9ce07363c..34253e3d4f 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
@@ -25,8 +25,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
}
- public override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
-
///
/// Apply a judgement result.
///
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index 0e1fe56cea..ed6f8a9a6a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -71,8 +71,8 @@ namespace osu.Game.Rulesets.Osu.Objects
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired
- ? new SpinnerTick { StartTime = startTime }
- : new SpinnerBonusTick { StartTime = startTime });
+ ? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
+ : new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration });
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
index 650d02c675..c890f3771b 100644
--- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
@@ -11,10 +11,17 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerTick : OsuHitObject
{
+ ///
+ /// Duration of the containing this spinner tick.
+ ///
+ public double SpinnerDuration { get; set; }
+
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+ public override double MaximumJudgementOffset => SpinnerDuration;
+
public class OsuSpinnerTickJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.SmallBonus;
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
index 45e25ee7dc..abecd19545 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
@@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick), _ => new TickPiece());
- public override double MaximumJudgementOffset => HitObject.HitWindow;
-
protected override void OnApply()
{
base.OnApply();
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs
index 433fdab908..6bcb8674e6 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs
@@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+ public override double MaximumJudgementOffset => HitWindow;
+
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
index e184d50d7c..6770309a7d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
@@ -1,10 +1,12 @@
// 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.Diagnostics;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Timing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -14,16 +16,23 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
+using osu.Game.Storyboards;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
{
- private TestGameplaySampleTriggerSource sampleTriggerSource;
+ private TestGameplaySampleTriggerSource sampleTriggerSource = null!;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
- private Beatmap beatmap;
+ private Beatmap beatmap = null!;
+
+ [Resolved]
+ private AudioManager audio { get; set; } = null!;
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
+ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
@@ -39,12 +48,13 @@ namespace osu.Game.Tests.Visual.Gameplay
const double start_offset = 8000;
const double spacing = 2000;
+ // intentionally start objects a bit late so we can test the case of no alive objects.
double t = start_offset;
+
beatmap.HitObjects.AddRange(new[]
{
new HitCircle
{
- // intentionally start objects a bit late so we can test the case of no alive objects.
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
},
@@ -80,42 +90,66 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestCorrectHitObject()
{
- HitObjectLifetimeEntry nextObjectEntry = null;
+ waitForAliveObjectIndex(null);
+ checkValidObjectIndex(0);
- AddAssert("no alive objects", () => getNextAliveObject() == null);
+ seekBeforeIndex(0);
+ waitForAliveObjectIndex(0);
+ checkValidObjectIndex(0);
- AddAssert("check initially correct object", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[0]);
+ AddAssert("first object not hit", () => getNextAliveObject()?.Entry?.Result?.HasResult != true);
- AddUntilStep("get next object", () =>
+ AddStep("hit first object", () =>
{
- var nextDrawableObject = getNextAliveObject();
+ var next = getNextAliveObject();
- if (nextDrawableObject != null)
+ if (next != null)
{
- nextObjectEntry = nextDrawableObject.Entry;
- InputManager.MoveMouseTo(nextDrawableObject.ScreenSpaceDrawQuad.Centre);
- return true;
+ Debug.Assert(next.Entry?.Result?.HasResult != true);
+
+ InputManager.MoveMouseTo(next.ScreenSpaceDrawQuad.Centre);
+ InputManager.Click(MouseButton.Left);
}
-
- return false;
});
- AddUntilStep("hit first hitobject", () =>
- {
- InputManager.Click(MouseButton.Left);
- return nextObjectEntry.Result?.HasResult == true;
- });
+ AddAssert("first object hit", () => getNextAliveObject()?.Entry?.Result?.HasResult == true);
- AddAssert("check correct object after hit", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[1]);
+ checkValidObjectIndex(1);
- AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[2]);
- AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
+ // Still object 1 as it's not hit yet.
+ seekBeforeIndex(1);
+ waitForAliveObjectIndex(1);
+ checkValidObjectIndex(1);
- AddUntilStep("no alive objects", () => getNextAliveObject() == null);
- AddAssert("check correct object after none alive", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
+ seekBeforeIndex(2);
+ waitForAliveObjectIndex(2);
+ checkValidObjectIndex(2);
+
+ seekBeforeIndex(3);
+ waitForAliveObjectIndex(3);
+ checkValidObjectIndex(3);
+
+ AddStep("Seek into future", () => Beatmap.Value.Track.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000));
+
+ waitForAliveObjectIndex(null);
+ checkValidObjectIndex(3);
}
- private DrawableHitObject getNextAliveObject() =>
+ private void seekBeforeIndex(int index) =>
+ AddStep($"seek to just before object {index}", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[index].StartTime - 100));
+
+ private void waitForAliveObjectIndex(int? index)
+ {
+ if (index == null)
+ AddUntilStep("wait for no alive objects", getNextAliveObject, () => Is.Null);
+ else
+ AddUntilStep($"wait for next alive to be {index}", () => getNextAliveObject()?.HitObject, () => Is.EqualTo(beatmap.HitObjects[index.Value]));
+ }
+
+ private void checkValidObjectIndex(int index) =>
+ AddAssert($"check valid object is {index}", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index]));
+
+ private DrawableHitObject? getNextAliveObject() =>
Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault();
[Test]
diff --git a/osu.Game/Database/LegacyExporter.cs b/osu.Game/Database/LegacyExporter.cs
index 09d6913dd9..7fe58240a2 100644
--- a/osu.Game/Database/LegacyExporter.cs
+++ b/osu.Game/Database/LegacyExporter.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using osu.Framework.Platform;
using osu.Game.Extensions;
using osu.Game.Utils;
@@ -43,7 +44,10 @@ namespace osu.Game.Database
{
string itemFilename = GetFilename(item).GetValidFilename();
- IEnumerable existingExports = exportStorage.GetFiles("", $"{itemFilename}*{FileExtension}");
+ IEnumerable existingExports =
+ exportStorage
+ .GetFiles(string.Empty, $"{itemFilename}*{FileExtension}")
+ .Concat(exportStorage.GetDirectories(string.Empty));
string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}");
using (var stream = exportStorage.CreateFileSafely(filename))
diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs
index 25830c9d54..6f6292c3b2 100644
--- a/osu.Game/Graphics/Containers/UserDimContainer.cs
+++ b/osu.Game/Graphics/Containers/UserDimContainer.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers
///
public const float BREAK_LIGHTEN_AMOUNT = 0.3f;
- protected const double BACKGROUND_FADE_DURATION = 800;
+ public const double BACKGROUND_FADE_DURATION = 800;
///
/// Whether or not user-configured settings relating to brightness of elements should be ignored
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 39f0888882..40b51ddcca 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -650,18 +650,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
UpdateResult(false);
}
- ///
- /// The maximum offset from the end time of at which this can be judged.
- /// The time offset of will be clamped to this value during .
- ///
- /// Defaults to the miss window of .
- ///
- ///
- ///
- /// This does not affect the time offset provided to invocations of .
- ///
- public virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0;
-
///
/// Applies the of this , notifying responders such as
/// the of the .
@@ -683,7 +671,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
$"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}]).");
}
- Result.TimeOffset = Math.Min(MaximumJudgementOffset, Time.Current - HitObject.GetEndTime());
+ Result.TimeOffset = Math.Min(HitObject.MaximumJudgementOffset, Time.Current - HitObject.GetEndTime());
if (Result.HasResult)
updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index 0f79e58201..25f538d211 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -200,6 +200,14 @@ namespace osu.Game.Rulesets.Objects
[NotNull]
protected virtual HitWindows CreateHitWindows() => new HitWindows();
+ ///
+ /// The maximum offset from the end time of at which this can be judged.
+ ///
+ /// Defaults to the miss window.
+ ///
+ ///
+ public virtual double MaximumJudgementOffset => HitWindows?.WindowFor(HitResult.Miss) ?? 0;
+
public IList CreateSlidingSamples()
{
var slidingSamples = new List();
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 71b452c309..2f8b101cfc 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.UI
public override void RequestResume(Action continueResume)
{
- if (ResumeOverlay != null && (Cursor == null || (Cursor.LastFrameState == Visibility.Visible && Contains(Cursor.ActiveCursor.ScreenSpaceDrawQuad.Centre))))
+ if (ResumeOverlay != null && UseResumeOverlay && (Cursor == null || (Cursor.LastFrameState == Visibility.Visible && Contains(Cursor.ActiveCursor.ScreenSpaceDrawQuad.Centre))))
{
ResumeOverlay.GameplayCursor = Cursor;
ResumeOverlay.ResumeAction = continueResume;
@@ -507,6 +507,15 @@ namespace osu.Game.Rulesets.UI
///
public ResumeOverlay ResumeOverlay { get; protected set; }
+ ///
+ /// Whether the should be used to return the user's cursor position to its previous location after a pause.
+ ///
+ ///
+ /// Defaults to true.
+ /// Even if true, will not have any effect if the ruleset does not have a resume overlay (see ).
+ ///
+ public bool UseResumeOverlay { get; set; } = true;
+
///
/// Returns first available provided by a .
///
@@ -531,6 +540,11 @@ namespace osu.Game.Rulesets.UI
}
}
+ ///
+ /// Create an optional resume overlay, which is displayed when a player requests to resume gameplay during non-break time.
+ /// This can be used to force the player to return their hands / cursor to the position they left off, to avoid players
+ /// using pauses as a means of adjusting their inputs (aka "pause buffering").
+ ///
protected virtual ResumeOverlay CreateResumeOverlay() => null;
///
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 3559a1521c..b93a427196 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -232,8 +232,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
double computedStartTime = computeDisplayStartTime(entry);
// always load the hitobject before its first judgement offset
- double judgementOffset = entry.HitObject.HitWindows?.WindowFor(Scoring.HitResult.Miss) ?? 0;
- entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - judgementOffset, computedStartTime);
+ entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - entry.HitObject.MaximumJudgementOffset, computedStartTime);
}
private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null)
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index d9062da8aa..4f7e4add32 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -417,7 +417,7 @@ namespace osu.Game.Screens.Play
lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in)
- ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint));
+ ApplyToBackground(b => b.FadeColour(Color4.White, 800, Easing.OutQuint));
}
protected virtual void ContentOut()
diff --git a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs
index d6c8a0ad6a..a5d1961bd8 100644
--- a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs
+++ b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs
@@ -1,9 +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.Diagnostics;
+using osu.Framework.Screens;
using osu.Game.Screens.Backgrounds;
namespace osu.Game.Screens.Play
@@ -12,6 +12,11 @@ namespace osu.Game.Screens.Play
{
protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value);
- public void ApplyToBackground(Action action) => base.ApplyToBackground(b => action.Invoke((BackgroundScreenBeatmap)b));
+ public void ApplyToBackground(Action action)
+ {
+ Debug.Assert(this.IsCurrentScreen());
+
+ base.ApplyToBackground(b => action.Invoke((BackgroundScreenBeatmap)b));
+ }
}
}
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 2d3d0f88d7..661eec8e97 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -31,6 +31,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
+using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
@@ -148,7 +149,7 @@ namespace osu.Game.Screens.Select
if (!this.IsCurrentScreen())
return;
- ApplyToBackground(b => b.BlurAmount.Value = e.NewValue ? BACKGROUND_BLUR : 0);
+ ApplyToBackground(applyBlurToBackground);
});
LoadComponentAsync(Carousel = new BeatmapCarousel
@@ -194,6 +195,7 @@ namespace osu.Game.Screens.Select
{
ParallaxAmount = 0.005f,
RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = new WedgeBackground
@@ -763,12 +765,18 @@ namespace osu.Game.Screens.Select
/// The working beatmap.
private void updateComponentFromBeatmap(WorkingBeatmap beatmap)
{
- ApplyToBackground(backgroundModeBeatmap =>
+ // If not the current screen, this will be applied in OnResuming.
+ if (this.IsCurrentScreen())
{
- backgroundModeBeatmap.Beatmap = beatmap;
- backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? BACKGROUND_BLUR : 0f;
- backgroundModeBeatmap.FadeColour(Color4.White, 250);
- });
+ ApplyToBackground(backgroundModeBeatmap =>
+ {
+ backgroundModeBeatmap.Beatmap = beatmap;
+ backgroundModeBeatmap.IgnoreUserSettings.Value = true;
+ backgroundModeBeatmap.FadeColour(Color4.White, 250);
+
+ applyBlurToBackground(backgroundModeBeatmap);
+ });
+ }
beatmapInfoWedge.Beatmap = beatmap;
@@ -785,6 +793,14 @@ namespace osu.Game.Screens.Select
}
}
+ private void applyBlurToBackground(BackgroundScreenBeatmap backgroundModeBeatmap)
+ {
+ backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? BACKGROUND_BLUR : 0f;
+ backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = configBackgroundBlur.Value ? 0 : 0.4f;
+
+ wedgeBackground.FadeTo(configBackgroundBlur.Value ? 0.5f : 0.2f, UserDimContainer.BACKGROUND_FADE_DURATION, Easing.OutQuint);
+ }
+
private readonly WeakReference lastTrack = new WeakReference(null);
///
diff --git a/osu.Game/Screens/Select/WedgeBackground.cs b/osu.Game/Screens/Select/WedgeBackground.cs
index da12c1a67a..2e2b43cd70 100644
--- a/osu.Game/Screens/Select/WedgeBackground.cs
+++ b/osu.Game/Screens/Select/WedgeBackground.cs
@@ -1,14 +1,11 @@
// 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 osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osuTK;
using osuTK.Graphics;
-using osu.Framework.Graphics.Shapes;
namespace osu.Game.Screens.Select
{
@@ -22,7 +19,7 @@ namespace osu.Game.Screens.Select
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1, 0.5f),
- Colour = Color4.Black.Opacity(0.5f),
+ Colour = Color4.Black,
Shear = new Vector2(0.15f, 0),
EdgeSmoothness = new Vector2(2, 0),
},
@@ -32,7 +29,7 @@ namespace osu.Game.Screens.Select
RelativePositionAxes = Axes.Y,
Size = new Vector2(1, -0.5f),
Position = new Vector2(0, 1),
- Colour = Color4.Black.Opacity(0.5f),
+ Colour = Color4.Black,
Shear = new Vector2(-0.15f, 0),
EdgeSmoothness = new Vector2(2, 0),
},
diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs
index 7b4d51ee83..737b222b9d 100644
--- a/osu.Game/Skinning/ArgonSkin.cs
+++ b/osu.Game/Skinning/ArgonSkin.cs
@@ -90,8 +90,8 @@ namespace osu.Game.Skinning
switch (lookup)
{
- case SkinComponentsContainerLookup componentLookup:
- switch (componentLookup.Target)
+ case SkinComponentsContainerLookup containerLookup:
+ switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.SongSelect:
var songSelectComponents = new DefaultSkinComponentsContainer(_ =>
diff --git a/osu.Game/Skinning/GameplaySkinComponentLookup.cs b/osu.Game/Skinning/GameplaySkinComponentLookup.cs
index 984df3f5bc..a44bf3a43d 100644
--- a/osu.Game/Skinning/GameplaySkinComponentLookup.cs
+++ b/osu.Game/Skinning/GameplaySkinComponentLookup.cs
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Skinning
{
///
- /// A lookup typed intended for use for skinnable gameplay components (not HUD level components).
+ /// A lookup type intended for use for skinnable gameplay components (not HUD level components).
///
///
/// The most common usage of this class is for ruleset-specific skinning implementations, but it can also be used directly
diff --git a/osu.Game/Skinning/ISerialisableDrawable.cs b/osu.Game/Skinning/ISerialisableDrawable.cs
index 65ece96a67..503b44c2dd 100644
--- a/osu.Game/Skinning/ISerialisableDrawable.cs
+++ b/osu.Game/Skinning/ISerialisableDrawable.cs
@@ -10,13 +10,14 @@ using osu.Game.Configuration;
namespace osu.Game.Skinning
{
///
- /// A drawable which can be serialised to a skin, placed and customised via the skin layout editor.
+ /// A drawable which is intended to be serialised to .
///
///
- /// Attaching this interface to any will make it serialisable to user skins (see ).
- /// Adding annotated bindables will also allow serialising settings automatically.
+ /// This is currently used exclusively for serialisation to a skin, and leaned on heavily to allow placement and customisation in the skin layout editor.
+ /// That said, it is intended to be flexible enough to potentially be used in other places we want to serialise drawables in the future.
///
- /// Serialisation is done via using .
+ /// Attaching this interface to any will make it serialisable via .
+ /// Adding annotated bindables will also allow serialising settings automatically.
///
public interface ISerialisableDrawable : IDrawable
{
diff --git a/osu.Game/Skinning/ISkinSource.cs b/osu.Game/Skinning/ISkinSource.cs
index b05d52d47c..0dfc99e3bc 100644
--- a/osu.Game/Skinning/ISkinSource.cs
+++ b/osu.Game/Skinning/ISkinSource.cs
@@ -7,7 +7,7 @@ using System.Collections.Generic;
namespace osu.Game.Skinning
{
///
- /// An abstract skin implementation which generally provides access to more than one skins (with fallback logic).
+ /// An abstract skin implementation, whose primary purpose is to properly handle component fallback across multiple layers of skins (e.g.: beatmap skin, user skin, default skin).
///
///
/// Common usage is to do an initial lookup via , and use the returned
diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs
index 84ca6ed967..1cbfda16cf 100644
--- a/osu.Game/Skinning/LegacyBeatmapSkin.cs
+++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs
@@ -45,9 +45,9 @@ namespace osu.Game.Skinning
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
- if (lookup is SkinComponentsContainerLookup targetComponent)
+ if (lookup is SkinComponentsContainerLookup containerLookup)
{
- switch (targetComponent.Target)
+ switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
// this should exist in LegacySkin instead, but there isn't a fallback skin for LegacySkins yet.
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index c581fcdfb6..98d9f1d617 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -343,8 +343,8 @@ namespace osu.Game.Skinning
switch (lookup)
{
- case SkinComponentsContainerLookup componentLookup:
- switch (componentLookup.Target)
+ case SkinComponentsContainerLookup containerLookup:
+ switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container =>
diff --git a/osu.Game/Skinning/SerialisableDrawableExtensions.cs b/osu.Game/Skinning/SerialisableDrawableExtensions.cs
index 5e8c2843a9..51b57a000d 100644
--- a/osu.Game/Skinning/SerialisableDrawableExtensions.cs
+++ b/osu.Game/Skinning/SerialisableDrawableExtensions.cs
@@ -22,9 +22,9 @@ namespace osu.Game.Skinning
component.Anchor = drawableInfo.Anchor;
component.Origin = drawableInfo.Origin;
- if (component is ISerialisableDrawable skinnable)
+ if (component is ISerialisableDrawable serialisableDrawable)
{
- skinnable.UsesFixedAnchor = drawableInfo.UsesFixedAnchor;
+ serialisableDrawable.UsesFixedAnchor = drawableInfo.UsesFixedAnchor;
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
@@ -37,7 +37,7 @@ namespace osu.Game.Skinning
continue;
}
- skinnable.CopyAdjustedSetting(bindable, settingValue);
+ serialisableDrawable.CopyAdjustedSetting(bindable, settingValue);
}
}
diff --git a/osu.Game/Skinning/SerialisedDrawableInfo.cs b/osu.Game/Skinning/SerialisedDrawableInfo.cs
index f571f1a945..29078f0d74 100644
--- a/osu.Game/Skinning/SerialisedDrawableInfo.cs
+++ b/osu.Game/Skinning/SerialisedDrawableInfo.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 System;
using System.Collections.Generic;
using System.Linq;
@@ -28,7 +26,7 @@ namespace osu.Game.Skinning
[Serializable]
public sealed class SerialisedDrawableInfo
{
- public Type Type { get; set; }
+ public Type Type { get; set; } = null!;
public Vector2 Position { get; set; }
@@ -66,8 +64,8 @@ namespace osu.Game.Skinning
Anchor = component.Anchor;
Origin = component.Origin;
- if (component is ISerialisableDrawable skinnable)
- UsesFixedAnchor = skinnable.UsesFixedAnchor;
+ if (component is ISerialisableDrawable serialisableDrawable)
+ UsesFixedAnchor = serialisableDrawable.UsesFixedAnchor;
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs
index 977fab1b7e..f261937012 100644
--- a/osu.Game/Skinning/Skin.cs
+++ b/osu.Game/Skinning/Skin.cs
@@ -165,8 +165,8 @@ namespace osu.Game.Skinning
case SkinnableSprite.SpriteComponentLookup sprite:
return this.GetAnimation(sprite.LookupName, false, false);
- case SkinComponentsContainerLookup componentLookup:
- if (!DrawableComponentInfo.TryGetValue(componentLookup.Target, out var skinnableInfo))
+ case SkinComponentsContainerLookup containerLookup:
+ if (!DrawableComponentInfo.TryGetValue(containerLookup.Target, out var skinnableInfo))
return null;
var components = new List();
diff --git a/osu.Game/Skinning/SkinComponentsContainer.cs b/osu.Game/Skinning/SkinComponentsContainer.cs
index 2620b40729..7d7a4965f6 100644
--- a/osu.Game/Skinning/SkinComponentsContainer.cs
+++ b/osu.Game/Skinning/SkinComponentsContainer.cs
@@ -24,6 +24,9 @@ namespace osu.Game.Skinning
{
private Container? content;
+ ///
+ /// The lookup criteria which will be used to retrieve components from the active skin.
+ ///
public SkinComponentsContainerLookup Lookup { get; }
public IBindableList Components => components;
diff --git a/osu.Game/Skinning/SkinComponentsContainerLookup.cs b/osu.Game/Skinning/SkinComponentsContainerLookup.cs
index ac6e98b23a..b0b87f18b5 100644
--- a/osu.Game/Skinning/SkinComponentsContainerLookup.cs
+++ b/osu.Game/Skinning/SkinComponentsContainerLookup.cs
@@ -3,8 +3,14 @@
namespace osu.Game.Skinning
{
+ ///
+ /// Represents a lookup of a collection of elements that make up a particular skinnable of the game.
+ ///
public class SkinComponentsContainerLookup : ISkinComponentLookup
{
+ ///
+ /// The target area / layer of the game for which skin components will be returned.
+ ///
public readonly TargetArea Target;
public SkinComponentsContainerLookup(TargetArea target)
@@ -12,6 +18,9 @@ namespace osu.Game.Skinning
Target = target;
}
+ ///
+ /// Represents a particular area or part of a game screen whose layout can be customised using the skin editor.
+ ///
public enum TargetArea
{
MainHUDComponents,
diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs
index 5e0712012e..12cdb71744 100644
--- a/osu.Game/Skinning/TrianglesSkin.cs
+++ b/osu.Game/Skinning/TrianglesSkin.cs
@@ -68,8 +68,8 @@ namespace osu.Game.Skinning
switch (lookup)
{
- case SkinComponentsContainerLookup componentLookup:
- switch (componentLookup.Target)
+ case SkinComponentsContainerLookup containerLookup:
+ switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.SongSelect:
var songSelectComponents = new DefaultSkinComponentsContainer(_ =>
diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs
index 0559d80384..aa5b506343 100644
--- a/osu.Game/Tests/Visual/ModTestScene.cs
+++ b/osu.Game/Tests/Visual/ModTestScene.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual
protected override bool CheckModsAllowFailure() => allowFail;
public ModTestPlayer(ModTestData data, bool allowFail)
- : base(false, false)
+ : base(true, false)
{
this.allowFail = allowFail;
currentTestData = data;