1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 16:02:55 +08:00

Merge branch 'master' into skin-editor-layer-select

This commit is contained in:
Bartłomiej Dach 2023-02-20 21:33:48 +01:00 committed by GitHub
commit 6799492596
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 618 additions and 262 deletions

View File

@ -60,26 +60,24 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestCatcherHyperStateReverted()
{
DrawableCatchHitObject drawableObject1 = null;
DrawableCatchHitObject drawableObject2 = null;
JudgementResult result1 = null;
JudgementResult result2 = null;
AddStep("catch hyper fruit", () =>
{
attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }, out drawableObject1, out result1);
result1 = attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } });
});
AddStep("catch normal fruit", () =>
{
attemptCatch(new Fruit(), out drawableObject2, out result2);
result2 = attemptCatch(new Fruit());
});
AddStep("revert second result", () =>
{
catcher.OnRevertResult(drawableObject2, result2);
catcher.OnRevertResult(result2);
});
checkHyperDash(true);
AddStep("revert first result", () =>
{
catcher.OnRevertResult(drawableObject1, result1);
catcher.OnRevertResult(result1);
});
checkHyperDash(false);
}
@ -87,16 +85,15 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestCatcherAnimationStateReverted()
{
DrawableCatchHitObject drawableObject = null;
JudgementResult result = null;
AddStep("catch kiai fruit", () =>
{
attemptCatch(new TestKiaiFruit(), out drawableObject, out result);
result = attemptCatch(new TestKiaiFruit());
});
checkState(CatcherAnimationState.Kiai);
AddStep("revert result", () =>
{
catcher.OnRevertResult(drawableObject, result);
catcher.OnRevertResult(result);
});
checkState(CatcherAnimationState.Idle);
}
@ -268,23 +265,19 @@ namespace osu.Game.Rulesets.Catch.Tests
private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state);
private void attemptCatch(CatchHitObject hitObject)
{
attemptCatch(() => hitObject, 1);
}
private void attemptCatch(Func<CatchHitObject> hitObject, int count)
{
for (int i = 0; i < count; i++)
attemptCatch(hitObject(), out _, out _);
attemptCatch(hitObject());
}
private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result)
private JudgementResult attemptCatch(CatchHitObject hitObject)
{
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableObject = createDrawableObject(hitObject);
result = createResult(hitObject);
var drawableObject = createDrawableObject(hitObject);
var result = createResult(hitObject);
applyResult(drawableObject, result);
return result;
}
private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result)

View File

@ -63,12 +63,12 @@ namespace osu.Game.Rulesets.Catch.UI
updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value);
}
public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result)
public void OnRevertResult(JudgementResult result)
{
if (!result.Type.AffectsCombo() || !result.HasResult)
return;
updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value);
updateCombo(result.ComboAtJudgement, null);
}
private void updateCombo(int newCombo, Color4? hitObjectColour)

View File

@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.UI
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result);
private void onRevertResult(JudgementResult result)
=> CatcherArea.OnRevertResult(result);
}
}

View File

@ -254,7 +254,7 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result)
public void OnRevertResult(JudgementResult result)
{
var catchResult = (CatchJudgementResult)result;
@ -268,8 +268,8 @@ namespace osu.Game.Rulesets.Catch.UI
SetHyperDashState();
}
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
caughtObjectContainer.RemoveAll(d => d.HitObject == result.HitObject, false);
droppedObjectTarget.RemoveAll(d => d.HitObject == result.HitObject, false);
}
/// <summary>

View File

@ -73,10 +73,10 @@ namespace osu.Game.Rulesets.Catch.UI
comboDisplay.OnNewResult(hitObject, result);
}
public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result)
public void OnRevertResult(JudgementResult result)
{
comboDisplay.OnRevertResult(hitObject, result);
Catcher.OnRevertResult(hitObject, result);
comboDisplay.OnRevertResult(result);
Catcher.OnRevertResult(result);
}
protected override void Update()

View File

@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public partial class TestSceneObjectPlacement : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[Test]
public void TestPlacementBeforeTrackStart()
{
AddStep("Seek to 0", () => EditorClock.Seek(0));
AddStep("Select note", () => InputManager.Key(Key.Number2));
AddStep("Hover negative span", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<Container>().First(x => x.Name == "Icons").Children[0]);
});
AddStep("Click", () => InputManager.Click(MouseButton.Left));
AddAssert("No notes placed", () => EditorBeatmap.HitObjects.All(x => x.StartTime >= 0));
}
[Test]
public void TestSeekOnNotePlacement()
{
double? initialTime = null;
AddStep("store initial time", () => initialTime = EditorClock.CurrentTime);
AddStep("change seek setting to true", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, true));
placeObject();
AddUntilStep("wait for seek to complete", () => !EditorClock.IsSeeking);
AddAssert("seeked forward to object", () => EditorClock.CurrentTime, () => Is.GreaterThan(initialTime));
}
[Test]
public void TestNoSeekOnNotePlacement()
{
double? initialTime = null;
AddStep("store initial time", () => initialTime = EditorClock.CurrentTime);
AddStep("change seek setting to false", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, false));
placeObject();
AddAssert("not seeking", () => !EditorClock.IsSeeking);
AddAssert("time is unchanged", () => EditorClock.CurrentTime, () => Is.EqualTo(initialTime));
}
private void placeObject()
{
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to centre of last column", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last().ScreenSpaceDrawQuad.Centre));
AddStep("place note", () => InputManager.Click(MouseButton.Left));
}
}
}

View File

@ -1,30 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public partial class TestScenePlacementBeforeTrackStart : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
[Test]
public void TestPlacement()
{
AddStep("Seek to 0", () => EditorClock.Seek(0));
AddStep("Select note", () => InputManager.Key(Key.Number2));
AddStep("Hover negative span", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<Container>().First(x => x.Name == "Icons").Children[0]);
});
AddStep("Click", () => InputManager.Click(MouseButton.Left));
AddAssert("No notes placed", () => EditorBeatmap.HitObjects.All(x => x.StartTime >= 0));
}
}
}

View File

@ -69,8 +69,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
private double? releaseTime;
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
public DrawableHoldNote()
: this(null)
{

View File

@ -15,13 +15,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
public partial class DrawableHoldNoteTail : DrawableNote
{
/// <summary>
/// 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
/// </summary>
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)
{

View File

@ -81,6 +81,8 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary>
public TailNote Tail { get; private set; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
/// <summary>
/// The time between ticks of this hold.
/// </summary>

View File

@ -10,6 +10,15 @@ namespace osu.Game.Rulesets.Mania.Objects
{
public class TailNote : Note
{
/// <summary>
/// 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
/// </summary>
public const double RELEASE_WINDOW_LENIENCE = 1.5;
public override Judgement CreateJudgement() => new ManiaJudgement();
public override double MaximumJudgementOffset => base.MaximumJudgementOffset * RELEASE_WINDOW_LENIENCE;
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -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<OsuReplayFrame>().ToList();
drawableRuleset.UseResumeOverlay = false;
}
}
}

View File

@ -25,8 +25,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
}
public override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
/// <summary>
/// Apply a judgement result.
/// </summary>

View File

@ -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 });
}
}

View File

@ -11,10 +11,17 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerTick : OsuHitObject
{
/// <summary>
/// Duration of the <see cref="Spinner"/> containing this spinner tick.
/// </summary>
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;

View File

@ -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();

View File

@ -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

View File

@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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]

View File

@ -19,7 +19,6 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@ -37,6 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestDrawablePoolingRuleset drawableRuleset;
private TestPlayfield playfield => (TestPlayfield)drawableRuleset.Playfield;
[Test]
public void TestReusedWithHitObjectsSpacedFarApart()
{
@ -133,29 +134,49 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("no DHOs shown", () => !this.ChildrenOfType<DrawableTestHitObject>().Any());
}
[Test]
public void TestRevertResult()
{
ManualClock clock = null;
Beatmap beatmap;
createTest(beatmap = new Beatmap
{
HitObjects =
{
new TestHitObject { StartTime = 0 },
new TestHitObject { StartTime = 500 },
new TestHitObject { StartTime = 1000 },
}
}, 10, () => new FramedClock(clock = new ManualClock()));
AddStep("fast forward to end", () => clock.CurrentTime = beatmap.HitObjects[^1].GetEndTime() + 100);
AddUntilStep("all judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
AddStep("rewind to middle", () => clock.CurrentTime = beatmap.HitObjects[1].StartTime - 100);
AddUntilStep("some results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(1));
AddStep("fast forward to end", () => clock.CurrentTime = beatmap.HitObjects[^1].GetEndTime() + 100);
AddUntilStep("all judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
AddStep("disable frame stability", () => drawableRuleset.FrameStablePlayback = false);
AddStep("instant seek to start", () => clock.CurrentTime = beatmap.HitObjects[0].StartTime - 100);
AddAssert("all results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0));
}
[Test]
public void TestApplyHitResultOnKilled()
{
ManualClock clock = null;
bool anyJudged = false;
void onNewResult(JudgementResult _) => anyJudged = true;
var beatmap = new Beatmap();
beatmap.HitObjects.Add(new TestKilledHitObject { Duration = 20 });
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
AddStep("subscribe to new result", () =>
{
anyJudged = false;
drawableRuleset.NewResult += onNewResult;
});
AddStep("skip past object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() + 1000);
AddAssert("object judged", () => anyJudged);
AddStep("clean up", () => drawableRuleset.NewResult -= onNewResult);
AddAssert("object judged", () => playfield.JudgedObjects.Count == 1);
}
private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null)
@ -212,12 +233,24 @@ namespace osu.Game.Tests.Visual.Gameplay
private partial class TestPlayfield : Playfield
{
public readonly HashSet<HitObject> JudgedObjects = new HashSet<HitObject>();
private readonly int poolSize;
public TestPlayfield(int poolSize)
{
this.poolSize = poolSize;
AddInternal(HitObjectContainer);
NewResult += (_, r) =>
{
Assert.That(JudgedObjects, Has.No.Member(r.HitObject));
JudgedObjects.Add(r.HitObject);
};
RevertResult += r =>
{
Assert.That(JudgedObjects, Has.Member(r.HitObject));
JudgedObjects.Remove(r.HitObject);
};
}
[BackgroundDependencyLoader]

View File

@ -22,12 +22,18 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached(typeof(ScoreProcessor))]
private TestScoreProcessor scoreProcessor = new TestScoreProcessor();
private readonly OsuHitWindows hitWindows = new OsuHitWindows();
private readonly OsuHitWindows hitWindows;
private UnstableRateCounter counter;
private double prev;
public TestSceneUnstableRateCounter()
{
hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(5);
}
[SetUpSteps]
public void SetUp()
{

View File

@ -178,6 +178,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorDim, 0.25f, 0f, 0.75f, 0.25f);
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f);
SetDefault(OsuSetting.EditorShowHitMarkers, true);
SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true);
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
@ -374,6 +375,7 @@ namespace osu.Game.Configuration
SeasonalBackgroundMode,
EditorWaveformOpacity,
EditorShowHitMarkers,
EditorAutoSeekOnPlacement,
DiscordRichPresence,
AutomaticallyDownloadWhenSpectating,
ShowOnlineExplicitContent,

View File

@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers
/// </summary>
public const float BREAK_LIGHTEN_AMOUNT = 0.3f;
protected const double BACKGROUND_FADE_DURATION = 800;
public const double BACKGROUND_FADE_DURATION = 800;
/// <summary>
/// Whether or not user-configured settings relating to brightness of elements should be ignored

View File

@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ShowHitMarkers => new TranslatableString(getKey(@"show_hit_markers"), @"Show hit markers");
/// <summary>
/// "Automatically seek after placing objects"
/// </summary>
public static LocalisableString AutoSeekOnPlacement => new TranslatableString(getKey(@"auto_seek_on_placement"), @"Automatically seek after placing objects");
/// <summary>
/// "Timing"
/// </summary>

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
@ -27,7 +28,14 @@ namespace osu.Game.Overlays.Mods
public Color4 AccentColour
{
get => headerBackground.Colour;
set => headerBackground.Colour = value;
set
{
headerBackground.Colour = value;
var hsv = new Colour4(value.R, value.G, value.B, 1f).ToHSV();
var trianglesColour = Colour4.FromHSV(hsv.X, hsv.Y + 0.2f, hsv.Z - 0.1f);
triangles.Colour = ColourInfo.GradientVertical(trianglesColour, trianglesColour.MultiplyAlpha(0f));
}
}
/// <summary>
@ -44,6 +52,7 @@ namespace osu.Game.Overlays.Mods
private readonly Box headerBackground;
private readonly Container contentContainer;
private readonly Box contentBackground;
private readonly TrianglesV2 triangles;
private const float header_height = 42;
@ -73,6 +82,13 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.X,
Height = header_height + ModSelectPanel.CORNER_RADIUS
},
triangles = new TrianglesV2
{
RelativeSizeAxes = Axes.X,
Height = header_height,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Velocity = 0.7f,
},
headerText = new OsuTextFlowContainer(t =>
{
t.Font = OsuFont.TorusAlternate.With(size: 17);

View File

@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Notifications
}
}
public string CompletionText { get; set; } = "Task has completed!";
public LocalisableString CompletionText { get; set; } = "Task has completed!";
private float progress;

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Utils;
using osu.Game.Graphics;
@ -66,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
RelativeSizeAxes = Axes.Both,
Colour = colour.Gray1,
},
usableAreaContainer = new Container
usableAreaContainer = new UsableAreaContainer(handler)
{
Origin = Anchor.Centre,
Children = new Drawable[]
@ -225,4 +226,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input
tabletContainer.Scale = new Vector2(1 / adjust);
}
}
public partial class UsableAreaContainer : Container
{
private readonly Bindable<Vector2> areaOffset;
public UsableAreaContainer(ITabletHandler tabletHandler)
{
areaOffset = tabletHandler.AreaOffset.GetBoundCopy();
}
protected override bool OnDragStart(DragStartEvent e) => true;
protected override void OnDrag(DragEvent e)
{
var newPos = Position + e.Delta;
this.MoveTo(Vector2.Clamp(newPos, Vector2.Zero, Parent.Size));
}
protected override void OnDragEnd(DragEndEvent e)
{
areaOffset.Value = Position;
base.OnDragEnd(e);
}
}
}

View File

@ -5,12 +5,16 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -21,16 +25,25 @@ namespace osu.Game.Overlays.SkinEditor
{
private Container box = null!;
private Container outlineBox = null!;
private AnchorOriginVisualiser anchorOriginVisualiser = null!;
private OsuSpriteText label = null!;
private Drawable drawable => (Drawable)Item;
protected override bool ShouldBeAlive => drawable.IsAlive && Item.IsPresent;
[Resolved]
private OsuColour colours { get; set; } = null!;
private Quad drawableQuad;
public override Quad ScreenSpaceDrawQuad => drawableQuad;
public override Quad SelectionQuad => drawable.ScreenSpaceDrawQuad;
public override bool Contains(Vector2 screenSpacePos) => drawableQuad.Contains(screenSpacePos);
public override Vector2 ScreenSpaceSelectionPoint => drawable.ToScreenSpace(drawable.OriginPosition);
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) =>
drawableQuad.Contains(screenSpacePos);
public SkinBlueprint(ISerialisableDrawable component)
: base(component)
@ -38,7 +51,7 @@ namespace osu.Game.Overlays.SkinEditor
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuColour colours)
{
InternalChildren = new Drawable[]
{
@ -46,23 +59,26 @@ namespace osu.Game.Overlays.SkinEditor
{
Children = new Drawable[]
{
outlineBox = new Container
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 3,
BorderColour = Color4.White,
CornerRadius = 3,
BorderThickness = SelectionBox.BORDER_RADIUS / 2,
BorderColour = ColourInfo.GradientVertical(colours.Pink4.Darken(0.4f), colours.Pink4),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
Blending = BlendingParameters.Additive,
Alpha = 0.2f,
Colour = ColourInfo.GradientVertical(colours.Pink2, colours.Pink4),
AlwaysPresent = true,
},
}
},
new OsuSpriteText
label = new OsuSpriteText
{
Text = Item.GetType().Name,
Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
@ -86,6 +102,18 @@ namespace osu.Game.Overlays.SkinEditor
this.FadeInFromZero(200, Easing.OutQuint);
}
protected override bool OnHover(HoverEvent e)
{
updateSelectedState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateSelectedState();
base.OnHoverLost(e);
}
protected override void OnSelected()
{
// base logic hides selected blueprints when not selected, but skin blueprints don't do that.
@ -100,73 +128,73 @@ namespace osu.Game.Overlays.SkinEditor
private void updateSelectedState()
{
outlineBox.FadeColour(colours.Pink.Opacity(IsSelected ? 1 : 0.5f), 200, Easing.OutQuint);
outlineBox.Child.FadeTo(IsSelected ? 0.2f : 0, 200, Easing.OutQuint);
anchorOriginVisualiser.FadeTo(IsSelected ? 1 : 0, 200, Easing.OutQuint);
label.FadeTo(IsSelected || IsHovered ? 1 : 0, 200, Easing.OutQuint);
}
private Quad drawableQuad;
public override Quad ScreenSpaceDrawQuad => drawableQuad;
protected override void Update()
{
base.Update();
drawableQuad = drawable.ScreenSpaceDrawQuad;
var quad = ToLocalSpace(drawable.ScreenSpaceDrawQuad);
drawableQuad = drawable.ToScreenSpace(
drawable.DrawRectangle
.Inflate(SkinSelectionHandler.INFLATE_SIZE));
box.Position = drawable.ToSpaceOfOtherDrawable(Vector2.Zero, this);
box.Size = quad.Size;
var localSpaceQuad = ToLocalSpace(drawableQuad);
box.Position = localSpaceQuad.TopLeft;
box.Size = localSpaceQuad.Size;
box.Rotation = drawable.Rotation;
box.Scale = new Vector2(MathF.Sign(drawable.Scale.X), MathF.Sign(drawable.Scale.Y));
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => drawable.ReceivePositionalInputAt(screenSpacePos);
public override Vector2 ScreenSpaceSelectionPoint => drawable.ToScreenSpace(drawable.OriginPosition);
public override Quad SelectionQuad => drawable.ScreenSpaceDrawQuad;
}
internal partial class AnchorOriginVisualiser : CompositeDrawable
{
private readonly Drawable drawable;
private readonly Box originBox;
private Drawable originBox = null!;
private readonly Box anchorBox;
private readonly Box anchorLine;
private Drawable anchorBox = null!;
private Drawable anchorLine = null!;
public AnchorOriginVisualiser(Drawable drawable)
{
this.drawable = drawable;
}
InternalChildren = new Drawable[]
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Color4 anchorColour = colours.Red1;
Color4 originColour = colours.Red3;
InternalChildren = new[]
{
anchorLine = new Box
anchorLine = new Circle
{
Height = 2,
Height = 3f,
Origin = Anchor.CentreLeft,
Colour = Color4.Yellow,
EdgeSmoothness = Vector2.One
Colour = ColourInfo.GradientHorizontal(originColour.Opacity(0.5f), originColour),
},
originBox = new Box
originBox = new Circle
{
Colour = Color4.Red,
Colour = originColour,
Origin = Anchor.Centre,
Size = new Vector2(5),
Size = new Vector2(7),
},
anchorBox = new Box
anchorBox = new Circle
{
Colour = Color4.Red,
Colour = anchorColour,
Origin = Anchor.Centre,
Size = new Vector2(5),
Size = new Vector2(10),
},
};
}
private Vector2? anchorPosition;
private Vector2? originPositionInDrawableSpace;
protected override void Update()
{
base.Update();
@ -174,8 +202,13 @@ namespace osu.Game.Overlays.SkinEditor
if (drawable.Parent == null)
return;
originBox.Position = drawable.ToSpaceOfOtherDrawable(drawable.OriginPosition, this);
anchorBox.Position = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this);
var newAnchor = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this);
anchorPosition = tweenPosition(anchorPosition ?? newAnchor, newAnchor);
anchorBox.Position = anchorPosition.Value;
// for the origin, tween in the drawable's local space to avoid unwanted tweening when the drawable is being dragged.
originPositionInDrawableSpace = originPositionInDrawableSpace != null ? tweenPosition(originPositionInDrawableSpace.Value, drawable.OriginPosition) : drawable.OriginPosition;
originBox.Position = drawable.ToSpaceOfOtherDrawable(originPositionInDrawableSpace.Value, this);
var point1 = ToLocalSpace(anchorBox.ScreenSpaceDrawQuad.Centre);
var point2 = ToLocalSpace(originBox.ScreenSpaceDrawQuad.Centre);
@ -184,5 +217,11 @@ namespace osu.Game.Overlays.SkinEditor
anchorLine.Width = (point2 - point1).Length;
anchorLine.Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(point2.Y - point1.Y, point2.X - point1.X));
}
private Vector2 tweenPosition(Vector2 oldPosition, Vector2 newPosition)
=> new Vector2(
(float)Interpolation.DampContinuously(oldPosition.X, newPosition.X, 25, Clock.ElapsedFrameTime),
(float)Interpolation.DampContinuously(oldPosition.Y, newPosition.Y, 25, Clock.ElapsedFrameTime)
);
}
}

View File

@ -2,13 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Graphics;
using osu.Framework.Threading;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
@ -65,7 +66,8 @@ namespace osu.Game.Overlays.SkinEditor
fill.Add(new ToolboxComponentButton(instance, target)
{
RequestPlacement = t => RequestPlacement?.Invoke(t)
RequestPlacement = t => RequestPlacement?.Invoke(t),
Expanding = contractOtherButtons,
});
}
catch (DependencyNotRegisteredException)
@ -79,15 +81,29 @@ namespace osu.Game.Overlays.SkinEditor
}
}
private void contractOtherButtons(ToolboxComponentButton obj)
{
foreach (var b in fill.OfType<ToolboxComponentButton>())
{
if (b == obj)
continue;
b.Contract();
}
}
public partial class ToolboxComponentButton : OsuButton
{
public Action<Type>? RequestPlacement;
public Action<ToolboxComponentButton>? Expanding;
private readonly Drawable component;
private readonly CompositeDrawable? dependencySource;
private Container innerContainer = null!;
private ScheduledDelegate? expandContractAction;
private const float contracted_size = 60;
private const float expanded_size = 120;
@ -102,20 +118,45 @@ namespace osu.Game.Overlays.SkinEditor
Height = contracted_size;
}
private const double animation_duration = 500;
protected override bool OnHover(HoverEvent e)
{
this.Delay(300).ResizeHeightTo(expanded_size, 500, Easing.OutQuint);
expandContractAction?.Cancel();
expandContractAction = Scheduler.AddDelayed(() =>
{
this.ResizeHeightTo(expanded_size, animation_duration, Easing.OutQuint);
Expanding?.Invoke(this);
}, 100);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
this.ResizeHeightTo(contracted_size, 500, Easing.OutQuint);
expandContractAction?.Cancel();
// If no other component is selected for too long, force a contract.
// Otherwise we will generally contract when Contract() is called from outside.
expandContractAction = Scheduler.AddDelayed(Contract, 1000);
}
public void Contract()
{
// Cheap debouncing to avoid stacking animations.
// The only place this is nulled is at the end of this method.
if (expandContractAction == null)
return;
this.ResizeHeightTo(contracted_size, animation_duration, Easing.OutQuint);
expandContractAction?.Cancel();
expandContractAction = null;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours)
private void load(OverlayColourProvider colourProvider)
{
BackgroundColour = colourProvider.Background3;

View File

@ -17,6 +17,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Overlays;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
@ -70,6 +71,7 @@ namespace osu.Game.Rulesets.Edit
private FillFlowContainer togglesCollection;
private IBindable<bool> hasTiming;
private Bindable<bool> autoSeekOnPlacement;
protected HitObjectComposer(Ruleset ruleset)
: base(ruleset)
@ -80,8 +82,10 @@ namespace osu.Game.Rulesets.Edit
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
private void load(OverlayColourProvider colourProvider, OsuConfigManager config)
{
autoSeekOnPlacement = config.GetBindable<bool>(OsuSetting.EditorAutoSeekOnPlacement);
Config = Dependencies.Get<IRulesetConfigCache>().GetConfigFor(Ruleset);
try
@ -365,7 +369,7 @@ namespace osu.Game.Rulesets.Edit
{
EditorBeatmap.Add(hitObject);
if (EditorClock.CurrentTime < hitObject.StartTime)
if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime)
EditorClock.SeekSmoothlyTo(hitObject.StartTime);
}
}

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using JetBrains.Annotations;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@ -33,16 +34,30 @@ namespace osu.Game.Rulesets.Judgements
public readonly Judgement Judgement;
/// <summary>
/// The offset from a perfect hit at which this <see cref="JudgementResult"/> occurred.
/// The time at which this <see cref="JudgementResult"/> occurred.
/// Populated when this <see cref="JudgementResult"/> is applied via <see cref="DrawableHitObject.ApplyResult"/>.
/// </summary>
public double TimeOffset { get; internal set; }
/// <remarks>
/// This is used instead of <see cref="TimeAbsolute"/> to check whether this <see cref="JudgementResult"/> should be reverted.
/// </remarks>
internal double? RawTime { get; set; }
/// <summary>
/// The absolute time at which this <see cref="JudgementResult"/> occurred.
/// Equal to the (end) time of the <see cref="HitObject"/> + <see cref="TimeOffset"/>.
/// The offset of <see cref="TimeAbsolute"/> from the end time of <see cref="HitObject"/>, clamped by <see cref="osu.Game.Rulesets.Objects.HitObject.MaximumJudgementOffset"/>.
/// </summary>
public double TimeAbsolute => HitObject.GetEndTime() + TimeOffset;
public double TimeOffset
{
get => RawTime != null ? Math.Min(RawTime.Value - HitObject.GetEndTime(), HitObject.MaximumJudgementOffset) : 0;
internal set => RawTime = HitObject.GetEndTime() + value;
}
/// <summary>
/// The absolute time at which this <see cref="JudgementResult"/> occurred, clamped by the end time of <see cref="HitObject"/> plus <see cref="osu.Game.Rulesets.Objects.HitObject.MaximumJudgementOffset"/>.
/// </summary>
/// <remarks>
/// The end time of <see cref="HitObject"/> is returned if this result is not populated yet.
/// </remarks>
public double TimeAbsolute => RawTime != null ? Math.Min(RawTime.Value, HitObject.GetEndTime() + HitObject.MaximumJudgementOffset) : HitObject.GetEndTime();
/// <summary>
/// The combo prior to this <see cref="JudgementResult"/> occurring.
@ -83,6 +98,13 @@ namespace osu.Game.Rulesets.Judgements
{
HitObject = hitObject;
Judgement = judgement;
Reset();
}
internal void Reset()
{
Type = HitResult.None;
RawTime = null;
}
public override string ToString() => $"{Type} (Score:{Judgement.NumericResultFor(this)} HP:{Judgement.HealthIncreaseFor(this)} {Judgement})";

View File

@ -82,6 +82,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary>
/// Invoked by this or a nested <see cref="DrawableHitObject"/> prior to a <see cref="JudgementResult"/> being reverted.
/// </summary>
/// <remarks>
/// This is only invoked if this <see cref="DrawableHitObject"/> is alive when the result is reverted.
/// </remarks>
public event Action<DrawableHitObject, JudgementResult> OnRevertResult;
/// <summary>
@ -222,6 +225,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
ensureEntryHasResult();
entry.RevertResult += onRevertResult;
foreach (var h in HitObject.NestedHitObjects)
{
var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this);
@ -234,7 +239,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
OnNestedDrawableCreated?.Invoke(drawableNested);
drawableNested.OnNewResult += onNewResult;
drawableNested.OnRevertResult += onRevertResult;
drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;
// This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation().
@ -308,7 +312,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
foreach (var obj in nestedHitObjects)
{
obj.OnNewResult -= onNewResult;
obj.OnRevertResult -= onRevertResult;
obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
@ -317,6 +320,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
HitObject.DefaultsApplied -= onDefaultsApplied;
entry.RevertResult -= onRevertResult;
OnFree();
ParentHitObject = null;
@ -365,7 +370,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result);
private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result);
private void onRevertResult()
{
updateState(ArmedState.Idle);
OnRevertResult?.Invoke(this, Result);
}
private void onApplyCustomUpdateState(DrawableHitObject drawableHitObject, ArmedState state) => ApplyCustomUpdateState?.Invoke(drawableHitObject, state);
@ -577,26 +586,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
#endregion
protected override void Update()
{
base.Update();
if (Result != null && Result.HasResult)
{
double endTime = HitObject.GetEndTime();
if (Result.TimeOffset + endTime > Time.Current)
{
OnRevertResult?.Invoke(this, Result);
Result.TimeOffset = 0;
Result.Type = HitResult.None;
updateState(ArmedState.Idle);
}
}
}
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
protected override void UpdateAfterChildren()
@ -650,18 +639,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
UpdateResult(false);
}
/// <summary>
/// The maximum offset from the end time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> can be judged.
/// The time offset of <see cref="Result"/> will be clamped to this value during <see cref="ApplyResult"/>.
/// <para>
/// Defaults to the miss window of <see cref="HitObject"/>.
/// </para>
/// </summary>
/// <remarks>
/// This does not affect the time offset provided to invocations of <see cref="CheckForResult"/>.
/// </remarks>
public virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0;
/// <summary>
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
/// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>.
@ -683,7 +660,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.RawTime = Time.Current;
if (Result.HasResult)
updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);

View File

@ -200,6 +200,14 @@ namespace osu.Game.Rulesets.Objects
[NotNull]
protected virtual HitWindows CreateHitWindows() => new HitWindows();
/// <summary>
/// The maximum offset from the end time of <see cref="HitObject"/> at which this <see cref="HitObject"/> can be judged.
/// <para>
/// Defaults to the miss window.
/// </para>
/// </summary>
public virtual double MaximumJudgementOffset => HitWindows?.WindowFor(HitResult.Miss) ?? 0;
public IList<HitSampleInfo> CreateSlidingSamples()
{
var slidingSamples = new List<HitSampleInfo>();

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Judgements;
@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Objects
private readonly IBindable<double> startTimeBindable = new BindableDouble();
internal event Action? RevertResult;
/// <summary>
/// Creates a new <see cref="HitObjectLifetimeEntry"/>.
/// </summary>
@ -95,5 +98,7 @@ namespace osu.Game.Rulesets.Objects
/// Set <see cref="LifetimeEntry.LifetimeStart"/> using <see cref="InitialLifetimeOffset"/>.
/// </summary>
internal void SetInitialLifetime() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
internal void OnRevertResult() => RevertResult?.Invoke();
}
}

View File

@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.UI
playfield = new Lazy<Playfield>(() => CreatePlayfield().With(p =>
{
p.NewResult += (_, r) => NewResult?.Invoke(r);
p.RevertResult += (_, r) => RevertResult?.Invoke(r);
p.RevertResult += r => RevertResult?.Invoke(r);
}));
}
@ -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
/// </summary>
public ResumeOverlay ResumeOverlay { get; protected set; }
/// <summary>
/// Whether the <see cref="ResumeOverlay"/> should be used to return the user's cursor position to its previous location after a pause.
/// </summary>
/// <remarks>
/// Defaults to <c>true</c>.
/// Even if <c>true</c>, will not have any effect if the ruleset does not have a resume overlay (see <see cref="CreateResumeOverlay"/>).
/// </remarks>
public bool UseResumeOverlay { get; set; } = true;
/// <summary>
/// Returns first available <see cref="HitWindows"/> provided by a <see cref="HitObject"/>.
/// </summary>
@ -531,6 +540,11 @@ namespace osu.Game.Rulesets.UI
}
}
/// <summary>
/// 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").
/// </summary>
protected virtual ResumeOverlay CreateResumeOverlay() => null;
/// <summary>

View File

@ -28,11 +28,6 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public event Action<DrawableHitObject, JudgementResult> NewResult;
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted.
/// </summary>
public event Action<DrawableHitObject, JudgementResult> RevertResult;
/// <summary>
/// Invoked when a <see cref="HitObject"/> becomes used by a <see cref="DrawableHitObject"/>.
/// </summary>
@ -111,7 +106,6 @@ namespace osu.Game.Rulesets.UI
private void addDrawable(DrawableHitObject drawable)
{
drawable.OnNewResult += onNewResult;
drawable.OnRevertResult += onRevertResult;
bindStartTime(drawable);
AddInternal(drawable);
@ -120,7 +114,6 @@ namespace osu.Game.Rulesets.UI
private void removeDrawable(DrawableHitObject drawable)
{
drawable.OnNewResult -= onNewResult;
drawable.OnRevertResult -= onRevertResult;
unbindStartTime(drawable);
@ -154,7 +147,6 @@ namespace osu.Game.Rulesets.UI
#endregion
private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r);
private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r);
#region Comparator + StartTime tracking

View File

@ -22,6 +22,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osu.Game.Rulesets.Objects.Pooling;
using osu.Framework.Extensions.ObjectExtensions;
namespace osu.Game.Rulesets.UI
{
@ -35,9 +36,9 @@ namespace osu.Game.Rulesets.UI
public event Action<DrawableHitObject, JudgementResult> NewResult;
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted.
/// Invoked when a judgement result is reverted.
/// </summary>
public event Action<DrawableHitObject, JudgementResult> RevertResult;
public event Action<JudgementResult> RevertResult;
/// <summary>
/// The <see cref="DrawableHitObject"/> contained in this Playfield.
@ -98,6 +99,8 @@ namespace osu.Game.Rulesets.UI
private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager();
private readonly Stack<HitObjectLifetimeEntry> judgedEntries;
/// <summary>
/// Creates a new <see cref="Playfield"/>.
/// </summary>
@ -107,14 +110,15 @@ namespace osu.Game.Rulesets.UI
hitObjectContainerLazy = new Lazy<HitObjectContainer>(() => CreateHitObjectContainer().With(h =>
{
h.NewResult += (d, r) => NewResult?.Invoke(d, r);
h.RevertResult += (d, r) => RevertResult?.Invoke(d, r);
h.NewResult += onNewResult;
h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o);
h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o);
}));
entryManager.OnEntryAdded += onEntryAdded;
entryManager.OnEntryRemoved += onEntryRemoved;
judgedEntries = new Stack<HitObjectLifetimeEntry>();
}
[BackgroundDependencyLoader]
@ -224,7 +228,7 @@ namespace osu.Game.Rulesets.UI
otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements);
otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r);
otherPlayfield.RevertResult += (d, r) => RevertResult?.Invoke(d, r);
otherPlayfield.RevertResult += r => RevertResult?.Invoke(r);
otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h);
otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h);
@ -252,6 +256,18 @@ namespace osu.Game.Rulesets.UI
updatable.Update(this);
}
}
// When rewinding, revert future judgements in the reverse order.
while (judgedEntries.Count > 0)
{
var result = judgedEntries.Peek().Result;
Debug.Assert(result?.RawTime != null);
if (Time.Current >= result.RawTime.Value)
break;
revertResult(judgedEntries.Pop());
}
}
/// <summary>
@ -443,6 +459,25 @@ namespace osu.Game.Rulesets.UI
#endregion
private void onNewResult(DrawableHitObject drawable, JudgementResult result)
{
Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null);
judgedEntries.Push(drawable.Entry.AsNonNull());
NewResult?.Invoke(drawable, result);
}
private void revertResult(HitObjectLifetimeEntry entry)
{
var result = entry.Result;
Debug.Assert(result != null);
RevertResult?.Invoke(result);
entry.OnRevertResult();
result.Reset();
}
#region Editor logic
/// <summary>

View File

@ -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)

View File

@ -32,6 +32,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
public abstract partial class SelectionHandler<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IKeyBindingHandler<GlobalAction>, IHasContextMenu
{
/// <summary>
/// How much padding around the selection area is added.
/// </summary>
public const float INFLATE_SIZE = 5;
/// <summary>
/// The currently selected blueprints.
/// Should be used when operations are dealing directly with the visible blueprints.
@ -346,7 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
for (int i = 1; i < selectedBlueprints.Count; i++)
selectionRect = RectangleF.Union(selectionRect, ToLocalSpace(selectedBlueprints[i].SelectionQuad).AABBFloat);
selectionRect = selectionRect.Inflate(5f);
selectionRect = selectionRect.Inflate(INFLATE_SIZE);
SelectionBox.Position = selectionRect.Location;
SelectionBox.Size = selectionRect.Size;

View File

@ -185,6 +185,7 @@ namespace osu.Game.Screens.Edit
private Bindable<float> editorBackgroundDim;
private Bindable<bool> editorHitMarkers;
private Bindable<bool> editorAutoSeekOnPlacement;
public Editor(EditorLoader loader = null)
{
@ -272,6 +273,7 @@ namespace osu.Game.Screens.Edit
editorBackgroundDim = config.GetBindable<float>(OsuSetting.EditorDim);
editorHitMarkers = config.GetBindable<bool>(OsuSetting.EditorShowHitMarkers);
editorAutoSeekOnPlacement = config.GetBindable<bool>(OsuSetting.EditorAutoSeekOnPlacement);
AddInternal(new OsuContextMenuContainer
{
@ -329,6 +331,10 @@ namespace osu.Game.Screens.Edit
new ToggleMenuItem(EditorStrings.ShowHitMarkers)
{
State = { BindTarget = editorHitMarkers },
},
new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement)
{
State = { BindTarget = editorAutoSeekOnPlacement },
}
}
},

View File

@ -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()

View File

@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<BackgroundScreenBeatmap> action) => base.ApplyToBackground(b => action.Invoke((BackgroundScreenBeatmap)b));
public void ApplyToBackground(Action<BackgroundScreenBeatmap> action)
{
Debug.Assert(this.IsCurrentScreen());
base.ApplyToBackground(b => action.Invoke((BackgroundScreenBeatmap)b));
}
}
}

View File

@ -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
/// <param name="beatmap">The working beatmap.</param>
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<ITrack?> lastTrack = new WeakReference<ITrack?>(null);
/// <summary>

View File

@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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),
},

View File

@ -65,8 +65,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())
{

View File

@ -125,11 +125,15 @@ namespace osu.Game.Skinning
{
}
// if that fails, attempt to deserialise using the old naked list.
// Of note, the migration code below runs on read of skins, but there's nothing to
// force a rewrite after migration. Let's not remove these migration rules until we
// have something in place to ensure we don't end up breaking skins of users that haven't
// manually saved their skin since a change was implemented.
// If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list.
if (layoutInfo == null)
{
// handle namespace changes...
// can be removed 2023-01-31
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.SongProgress", @"osu.Game.Screens.Play.HUD.DefaultSongProgress");
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.LegacyComboCounter", @"osu.Game.Skinning.LegacyComboCounter");

View File

@ -74,17 +74,12 @@ namespace osu.Game.Skinning
cancellationSource?.Cancel();
cancellationSource = null;
if (content != null)
LoadComponentAsync(content, wrapper =>
{
LoadComponentAsync(content, wrapper =>
{
AddInternal(wrapper);
components.AddRange(wrapper.Children.OfType<ISerialisableDrawable>());
ComponentsLoaded = true;
}, (cancellationSource = new CancellationTokenSource()).Token);
}
else
AddInternal(wrapper);
components.AddRange(wrapper.Children.OfType<ISerialisableDrawable>());
ComponentsLoaded = true;
}, (cancellationSource = new CancellationTokenSource()).Token);
}
/// <inheritdoc cref="ISerialisableDrawableContainer"/>

View File

@ -18,10 +18,12 @@ namespace osu.Game.Skinning
/// </summary>
public readonly TargetArea Target;
/// <summary>
/// The ruleset for which skin components should be returned.
/// A <see langword="null"/> value means that returned components are global and should be applied for all rulesets.
/// </summary>
public readonly RulesetInfo? Ruleset;
public string GetSerialisableIdentifier() => Ruleset?.ShortName ?? "global";
public SkinComponentsContainerLookup(TargetArea target, RulesetInfo? ruleset = null)
{
Target = target;

View File

@ -17,8 +17,9 @@ namespace osu.Game.Skinning
[Serializable]
public class SkinLayoutInfo
{
private const string global_identifier = "global";
private const string global_identifier = @"global";
[JsonIgnore]
public IEnumerable<SerialisedDrawableInfo> AllDrawables => DrawableInfo.Values.SelectMany(v => v);
[JsonProperty]

View File

@ -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;