diff --git a/osu.Android.props b/osu.Android.props
index 75828147a5..0bf415e764 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
index 8472b995e8..5835ccaf78 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
@@ -4,8 +4,10 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
@@ -55,6 +57,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
}
});
+ [Test]
+ public void TestGameCursorHidden()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new CatchModRelax(),
+ Autoplay = false,
+ PassCondition = () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
+ return this.ChildrenOfType().Single().State.Value == Visibility.Hidden;
+ }
+ });
+ }
+
private bool passCondition()
{
var playfield = this.ChildrenOfType().Single();
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 4df297565e..184ff38cc6 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -3,13 +3,16 @@
#nullable disable
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -49,6 +52,14 @@ namespace osu.Game.Rulesets.Catch.UI
this.difficulty = difficulty;
}
+ protected override GameplayCursorContainer CreateCursor()
+ {
+ if (Mods != null && Mods.Any(m => m is ModRelax))
+ return new CatchRelaxCursorContainer();
+
+ return base.CreateCursor();
+ }
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game.Rulesets.Catch/UI/CatchRelaxCursorContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRelaxCursorContainer.cs
new file mode 100644
index 0000000000..f30b8f0f36
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatchRelaxCursorContainer.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public partial class CatchRelaxCursorContainer : GameplayCursorContainer
+ {
+ // Just hide the cursor in relax.
+ // The main goal here is to show that we have a cursor so the game never shows the global one.
+ protected override Drawable CreateCursor() => Empty();
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index 653c75baac..aca555552f 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
@@ -90,6 +90,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public override bool CursorInPlacementArea => false;
public TestHitObjectComposer(Playfield playfield)
+ : base(new ManiaRuleset())
{
Playfield = playfield;
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
index e412c47c09..73ee5df9dc 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
@@ -4,6 +4,8 @@
#nullable disable
using System;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
@@ -22,6 +24,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
private bool isPlacingEnd;
+ [Resolved(CanBeNull = true)]
+ [CanBeNull]
+ private IBeatSnapProvider beatSnapProvider { get; set; }
+
public SpinnerPlacementBlueprint()
: base(new Spinner { Position = OsuPlayfield.BASE_SIZE / 2 })
{
@@ -33,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
base.Update();
if (isPlacingEnd)
- HitObject.EndTime = Math.Max(HitObject.StartTime, EditorClock.CurrentTime);
+ updateEndTimeFromCurrent();
piece.UpdateFrom(HitObject);
}
@@ -45,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
if (e.Button != MouseButton.Right)
return false;
- HitObject.EndTime = EditorClock.CurrentTime;
+ updateEndTimeFromCurrent();
EndPlacement(true);
}
else
@@ -61,5 +67,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
return true;
}
+
+ private void updateEndTimeFromCurrent()
+ {
+ HitObject.EndTime = beatSnapProvider == null
+ ? Math.Max(HitObject.StartTime, EditorClock.CurrentTime)
+ : Math.Max(HitObject.StartTime + beatSnapProvider.GetBeatLengthAtTime(HitObject.StartTime), beatSnapProvider.SnapTime(EditorClock.CurrentTime));
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
index 55c20eebe9..77cf340b95 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
@@ -17,7 +17,6 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
@@ -196,8 +195,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private IEnumerable generateBeats(IBeatmap beatmap, IReadOnlyCollection originalHitObjects)
{
- double startTime = originalHitObjects.First().StartTime;
- double endTime = originalHitObjects.Last().GetEndTime();
+ double startTime = beatmap.HitObjects.First().StartTime;
+ double endTime = beatmap.GetLastObjectTime();
var beats = beatmap.ControlPointInfo.TimingPoints
// Ignore timing points after endTime
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoKiaiGlow.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoKiaiGlow.cs
new file mode 100644
index 0000000000..a5e2eb0dbb
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoKiaiGlow.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Taiko.Skinning.Legacy;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ public partial class TestSceneTaikoKiaiGlow : TaikoSkinnableTestScene
+ {
+ [Test]
+ public void TestKiaiGlow()
+ {
+ AddStep("Create kiai glow", () => SetContents(_ => new LegacyKiaiGlow()));
+ AddToggleStep("Toggle kiai mode", setUpBeatmap);
+ }
+
+ private void setUpBeatmap(bool withKiai)
+ {
+ var controlPointInfo = new ControlPointInfo();
+
+ controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+
+ if (withKiai)
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPointInfo
+ });
+
+ Beatmap.Value.Track.Start();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs
new file mode 100644
index 0000000000..623243e9e1
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
+{
+ internal partial class LegacyKiaiGlow : BeatSyncedContainer
+ {
+ private bool isKiaiActive;
+
+ private Sprite sprite = null!;
+
+ [BackgroundDependencyLoader(true)]
+ private void load(ISkinSource skin, HealthProcessor? healthProcessor)
+ {
+ Child = sprite = new Sprite
+ {
+ Texture = skin.GetTexture("taiko-glow"),
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Alpha = 0,
+ Scale = new Vector2(0.7f),
+ Colour = new Colour4(255, 228, 0, 255),
+ };
+
+ if (healthProcessor != null)
+ healthProcessor.NewJudgement += onNewJudgement;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (isKiaiActive)
+ sprite.Alpha = (float)Math.Min(1, sprite.Alpha + Math.Abs(Clock.ElapsedFrameTime) / 100f);
+ else
+ sprite.Alpha = (float)Math.Max(0, sprite.Alpha - Math.Abs(Clock.ElapsedFrameTime) / 600f);
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
+ {
+ isKiaiActive = effectPoint.KiaiMode;
+ }
+
+ private void onNewJudgement(JudgementResult result)
+ {
+ if (!result.IsHit || !isKiaiActive)
+ return;
+
+ sprite.ScaleTo(0.85f).Then()
+ .ScaleTo(0.7f, 80, Easing.OutQuad);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs
index 86175d3bca..85870d0fd6 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
private Sprite kiai = null!;
- private bool kiaiDisplayed;
+ private bool isKiaiActive;
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
@@ -41,17 +42,19 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
};
}
+ protected override void Update()
+ {
+ base.Update();
+
+ if (isKiaiActive)
+ kiai.Alpha = (float)Math.Min(1, kiai.Alpha + Math.Abs(Clock.ElapsedFrameTime) / 200f);
+ else
+ kiai.Alpha = (float)Math.Max(0, kiai.Alpha - Math.Abs(Clock.ElapsedFrameTime) / 200f);
+ }
+
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
- base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
-
- if (effectPoint.KiaiMode != kiaiDisplayed)
- {
- kiaiDisplayed = effectPoint.KiaiMode;
-
- kiai.ClearTransforms();
- kiai.FadeTo(kiaiDisplayed ? 1 : 0, 200);
- }
+ isKiaiActive = effectPoint.KiaiMode;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index 7bf99306f0..d61f9ac35d 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -129,6 +129,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.Mascot:
return new DrawableTaikoMascot();
+ case TaikoSkinComponents.KiaiGlow:
+ if (GetTexture("taiko-glow") != null)
+ return new LegacyKiaiGlow();
+
+ return null;
+
default:
throw new UnsupportedSkinComponentException(lookup);
}
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
index bf48898dd2..b8e3313e1b 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
@@ -21,5 +21,6 @@ namespace osu.Game.Rulesets.Taiko
TaikoExplosionKiai,
Scroller,
Mascot,
+ KiaiGlow
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 6ce0be5868..9493de624a 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -112,6 +112,10 @@ namespace osu.Game.Rulesets.Taiko.UI
FillMode = FillMode.Fit,
Children = new[]
{
+ new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.KiaiGlow), _ => Empty())
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
hitExplosionContainer = new Container
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index c6bdd25e8b..5787bd6066 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -314,6 +314,24 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestGetLastObjectTime()
+ {
+ var decoder = new LegacyBeatmapDecoder();
+
+ using (var resStream = TestResources.OpenResource("mania-last-object-not-latest.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var beatmap = decoder.Decode(stream);
+
+ Assert.That(beatmap.HitObjects.Last().StartTime, Is.EqualTo(2494));
+ Assert.That(beatmap.HitObjects.Last().GetEndTime(), Is.EqualTo(2494));
+
+ Assert.That(beatmap.HitObjects.Max(h => h.GetEndTime()), Is.EqualTo(2582));
+ Assert.That(beatmap.GetLastObjectTime(), Is.EqualTo(2582));
+ }
+ }
+
[Test]
public void TestDecodeBeatmapComboOffsetsOsu()
{
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
index 62863524fe..04fc4cafbd 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
@@ -137,6 +138,31 @@ namespace osu.Game.Tests.Gameplay
AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss);
}
+ [Test]
+ public void TestResultSetBeforeLoadComplete()
+ {
+ TestDrawableHitObject dho = null;
+ HitObjectLifetimeEntry lifetimeEntry = null;
+ AddStep("Create lifetime entry", () =>
+ {
+ var hitObject = new HitObject { StartTime = Time.Current };
+ lifetimeEntry = new HitObjectLifetimeEntry(hitObject)
+ {
+ Result = new JudgementResult(hitObject, hitObject.CreateJudgement())
+ {
+ Type = HitResult.Great
+ }
+ };
+ });
+ AddStep("Create DHO and apply entry", () =>
+ {
+ dho = new TestDrawableHitObject();
+ dho.Apply(lifetimeEntry);
+ Child = dho;
+ });
+ AddAssert("DHO state is correct", () => dho.State.Value, () => Is.EqualTo(ArmedState.Hit));
+ }
+
private partial class TestDrawableHitObject : DrawableHitObject
{
public const double INITIAL_LIFETIME_OFFSET = 100;
diff --git a/osu.Game.Tests/Resources/mania-last-object-not-latest.osu b/osu.Game.Tests/Resources/mania-last-object-not-latest.osu
new file mode 100644
index 0000000000..51893383d8
--- /dev/null
+++ b/osu.Game.Tests/Resources/mania-last-object-not-latest.osu
@@ -0,0 +1,39 @@
+osu file format v14
+
+[General]
+SampleSet: Normal
+StackLeniency: 0.7
+Mode: 3
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:5
+OverallDifficulty:8
+ApproachRate:8
+SliderMultiplier:3.59999990463257
+SliderTickRate:2
+
+[TimingPoints]
+24,352.941176470588,4,1,1,100,1,0
+6376,-50,4,1,1,100,0,0
+
+[HitObjects]
+51,192,24,1,0,0:0:0:0:
+153,192,200,1,0,0:0:0:0:
+358,192,376,1,0,0:0:0:0:
+460,192,553,1,0,0:0:0:0:
+460,192,729,128,0,1435:0:0:0:0:
+358,192,906,128,0,1612:0:0:0:0:
+256,192,1082,128,0,1788:0:0:0:0:
+153,192,1259,128,0,1965:0:0:0:0:
+51,192,1435,128,0,2141:0:0:0:0:
+51,192,2318,1,12,0:0:0:0:
+153,192,2318,1,4,0:0:0:0:
+256,192,2318,1,6,0:0:0:0:
+358,192,2318,1,14,0:0:0:0:
+460,192,2318,1,0,0:0:0:0:
+51,192,2494,128,0,2582:0:0:0:0:
+153,192,2494,128,14,2582:0:0:0:0:
+256,192,2494,128,6,2582:0:0:0:0:
+358,192,2494,128,4,2582:0:0:0:0:
+460,192,2494,1,12,0:0:0:0:0:
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index 94f9b4262d..29fadd151f 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -187,18 +188,22 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestInputDoesntWorkWhenHUDHidden()
{
- SongProgressBar getSongProgress() => hudOverlay.ChildrenOfType().Single();
+ SongProgressBar? getSongProgress() => hudOverlay.ChildrenOfType().SingleOrDefault();
bool seeked = false;
createNew();
+ AddUntilStep("wait for song progress", () => getSongProgress() != null);
+
AddStep("bind seek", () =>
{
seeked = false;
var progress = getSongProgress();
+ Debug.Assert(progress != null);
+
progress.ShowHandle = true;
progress.OnSeek += _ => seeked = true;
});
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
index c473278fdc..6ccf73d8ff 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
@@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
- AddAssert("state is available", () => downloadButton.State.Value == DownloadState.NotDownloaded);
+ checkState(DownloadState.NotDownloaded);
AddStep("click button", () =>
{
@@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
- AddAssert("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
+ checkState(DownloadState.NotDownloaded);
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
}
@@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
- AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
+ checkState(DownloadState.NotDownloaded);
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
}
@@ -174,17 +174,16 @@ namespace osu.Game.Tests.Visual.Gameplay
});
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
-
- AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
+ checkState(DownloadState.NotDownloaded);
AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true)));
- AddUntilStep("state is available", () => downloadButton.State.Value == DownloadState.LocallyAvailable);
+ checkState(DownloadState.LocallyAvailable);
AddAssert("button is enabled", () => downloadButton.ChildrenOfType().First().Enabled.Value);
AddStep("delete score", () => scoreManager.Delete(imported.Value));
- AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
+ checkState(DownloadState.NotDownloaded);
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
}
@@ -202,10 +201,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
- AddAssert("state is unknown", () => downloadButton.State.Value == DownloadState.Unknown);
+ checkState(DownloadState.Unknown);
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
}
+ private void checkState(DownloadState expectedState) =>
+ AddUntilStep($"state is {expectedState}", () => downloadButton.State.Value, () => Is.EqualTo(expectedState));
+
private ScoreInfo getScoreInfo(bool replayAvailable, bool hasOnlineId = true) => new ScoreInfo
{
OnlineID = hasOnlineId ? online_score_id : 0,
diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
index 7d2ac90939..ebd5e12acb 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
@@ -73,6 +73,11 @@ namespace osu.Game.Tests.Visual.Online
messageIdSequence = 0;
channelManager.CurrentChannel.Value = testChannel = new Channel();
+ reinitialiseDrawableDisplay();
+ });
+
+ private void reinitialiseDrawableDisplay()
+ {
Children = new[]
{
chatDisplay = new TestStandAloneChatDisplay
@@ -92,13 +97,14 @@ namespace osu.Game.Tests.Visual.Online
Channel = { Value = testChannel },
}
};
- });
+ }
[Test]
public void TestSystemMessageOrdering()
{
var standardMessage = new Message(messageIdSequence++)
{
+ Timestamp = DateTimeOffset.Now,
Sender = admin,
Content = "I am a wang!"
};
@@ -106,14 +112,45 @@ namespace osu.Game.Tests.Visual.Online
var infoMessage1 = new InfoMessage($"the system is calling {messageIdSequence++}");
var infoMessage2 = new InfoMessage($"the system is calling {messageIdSequence++}");
+ var standardMessage2 = new Message(messageIdSequence++)
+ {
+ Timestamp = DateTimeOffset.Now,
+ Sender = admin,
+ Content = "I am a wang!"
+ };
+
AddStep("message from admin", () => testChannel.AddNewMessages(standardMessage));
AddStep("message from system", () => testChannel.AddNewMessages(infoMessage1));
AddStep("message from system", () => testChannel.AddNewMessages(infoMessage2));
+ AddStep("message from admin", () => testChannel.AddNewMessages(standardMessage2));
- AddAssert("message order is correct", () => testChannel.Messages.Count == 3
- && testChannel.Messages[0] == standardMessage
- && testChannel.Messages[1] == infoMessage1
- && testChannel.Messages[2] == infoMessage2);
+ AddAssert("count is correct", () => testChannel.Messages.Count, () => Is.EqualTo(4));
+
+ AddAssert("message order is correct", () => testChannel.Messages, () => Is.EqualTo(new[]
+ {
+ standardMessage,
+ infoMessage1,
+ infoMessage2,
+ standardMessage2
+ }));
+
+ AddAssert("displayed order is correct", () => chatDisplay.DrawableChannel.ChildrenOfType().Select(c => c.Message), () => Is.EqualTo(new[]
+ {
+ standardMessage,
+ infoMessage1,
+ infoMessage2,
+ standardMessage2
+ }));
+
+ AddStep("reinit drawable channel", reinitialiseDrawableDisplay);
+
+ AddAssert("displayed order is still correct", () => chatDisplay.DrawableChannel.ChildrenOfType().Select(c => c.Message), () => Is.EqualTo(new[]
+ {
+ standardMessage,
+ infoMessage1,
+ infoMessage2,
+ standardMessage2
+ }));
}
[Test]
diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs
index 2d02fb6200..416d655cc3 100644
--- a/osu.Game/Beatmaps/Beatmap.cs
+++ b/osu.Game/Beatmaps/Beatmap.cs
@@ -81,9 +81,14 @@ namespace osu.Game.Beatmaps
public double GetMostCommonBeatLength()
{
+ double lastTime;
+
// The last playable time in the beatmap - the last timing point extends to this time.
// Note: This is more accurate and may present different results because osu-stable didn't have the ability to calculate slider durations in this context.
- double lastTime = HitObjects.LastOrDefault()?.GetEndTime() ?? ControlPointInfo.TimingPoints.LastOrDefault()?.Time ?? 0;
+ if (!HitObjects.Any())
+ lastTime = ControlPointInfo.TimingPoints.LastOrDefault()?.Time ?? 0;
+ else
+ lastTime = this.GetLastObjectTime();
var mostCommon =
// Construct a set of (beatLength, duration) tuples for each individual timing point.
diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs
index 0e892b6581..f6771f7adf 100644
--- a/osu.Game/Beatmaps/IBeatmap.cs
+++ b/osu.Game/Beatmaps/IBeatmap.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Collections.Generic;
+using System.Linq;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
@@ -102,5 +103,16 @@ namespace osu.Game.Beatmaps
addCombo(nested, ref combo);
}
}
+
+ ///
+ /// Find the absolute end time of the latest in a beatmap. Will throw if beatmap contains no objects.
+ ///
+ ///
+ /// This correctly accounts for rulesets which have concurrent hitobjects which may have durations, causing the .Last() object
+ /// to not necessarily have the latest end time.
+ ///
+ /// It's not super efficient so calls should be kept to a minimum.
+ ///
+ public static double GetLastObjectTime(this IBeatmap beatmap) => beatmap.HitObjects.Max(h => h.GetEndTime());
}
}
diff --git a/osu.Game/Online/Chat/InfoMessage.cs b/osu.Game/Online/Chat/InfoMessage.cs
index d98c67de34..2ade99dcb2 100644
--- a/osu.Game/Online/Chat/InfoMessage.cs
+++ b/osu.Game/Online/Chat/InfoMessage.cs
@@ -1,9 +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 osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.Chat
@@ -13,7 +10,6 @@ namespace osu.Game.Online.Chat
public InfoMessage(string message)
: base(null)
{
- Timestamp = DateTimeOffset.Now;
Content = message;
Sender = APIUser.SYSTEM_USER;
diff --git a/osu.Game/Online/Chat/LocalEchoMessage.cs b/osu.Game/Online/Chat/LocalEchoMessage.cs
index b226fe6cad..8a39515575 100644
--- a/osu.Game/Online/Chat/LocalEchoMessage.cs
+++ b/osu.Game/Online/Chat/LocalEchoMessage.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
-
namespace osu.Game.Online.Chat
{
public class LocalEchoMessage : LocalMessage
diff --git a/osu.Game/Online/Chat/LocalMessage.cs b/osu.Game/Online/Chat/LocalMessage.cs
index 5736f5cabf..57caca2287 100644
--- a/osu.Game/Online/Chat/LocalMessage.cs
+++ b/osu.Game/Online/Chat/LocalMessage.cs
@@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
+using System;
namespace osu.Game.Online.Chat
{
@@ -13,6 +13,7 @@ namespace osu.Game.Online.Chat
protected LocalMessage(long? id)
: base(id)
{
+ Timestamp = DateTimeOffset.Now;
}
}
}
diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs
index 9f6f9c8d6b..8ea3ca0fc7 100644
--- a/osu.Game/Online/Chat/Message.cs
+++ b/osu.Game/Online/Chat/Message.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses;
@@ -59,19 +60,28 @@ namespace osu.Game.Online.Chat
/// The s' and s are according to
public List Links;
+ private static long constructionOrderStatic;
+ private readonly long constructionOrder;
+
public Message(long? id)
{
Id = id;
+
+ constructionOrder = Interlocked.Increment(ref constructionOrderStatic);
}
public int CompareTo(Message other)
{
- if (!Id.HasValue)
- return other.Id.HasValue ? 1 : Timestamp.CompareTo(other.Timestamp);
- if (!other.Id.HasValue)
- return -1;
+ if (Id.HasValue && other.Id.HasValue)
+ return Id.Value.CompareTo(other.Id.Value);
- return Id.Value.CompareTo(other.Id.Value);
+ int timestampComparison = Timestamp.CompareTo(other.Timestamp);
+
+ if (timestampComparison != 0)
+ return timestampComparison;
+
+ // Timestamp might not be accurate enough to make a stable sorting decision.
+ return constructionOrder.CompareTo(other.constructionOrder);
}
public virtual bool Equals(Message other)
@@ -85,6 +95,6 @@ namespace osu.Game.Online.Chat
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
public override int GetHashCode() => Id.GetHashCode();
- public override string ToString() => $"[{ChannelId}] ({Id}) {Sender}: {Content}";
+ public override string ToString() => $"({(Id?.ToString() ?? "null")}) {Timestamp} {Sender}: {Content}";
}
}
diff --git a/osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs b/osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs
index a4762fdaed..a1e61e66f8 100644
--- a/osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Overlays.FirstRunSetup
loading.Hide();
tick.FadeIn(500, Easing.OutQuint);
- Background.FadeColour(colours.Green, 500, Easing.OutQuint);
+ this.TransformTo(nameof(BackgroundColour), colours.Green, 500, Easing.OutQuint);
progressBar.FillColour = colours.Green;
this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint);
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index aff242d63f..b5b7400f64 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -45,8 +45,6 @@ namespace osu.Game.Rulesets.Edit
{
protected IRulesetConfigManager Config { get; private set; }
- protected readonly Ruleset Ruleset;
-
// Provides `Playfield`
private DependencyContainer dependencies;
@@ -74,8 +72,8 @@ namespace osu.Game.Rulesets.Edit
private IBindable hasTiming;
protected HitObjectComposer(Ruleset ruleset)
+ : base(ruleset)
{
- Ruleset = ruleset;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
@@ -419,8 +417,11 @@ namespace osu.Game.Rulesets.Edit
[Cached]
public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
{
- protected HitObjectComposer()
+ public readonly Ruleset Ruleset;
+
+ protected HitObjectComposer(Ruleset ruleset)
{
+ Ruleset = ruleset;
RelativeSizeAxes = Axes.Both;
}
diff --git a/osu.Game/Rulesets/Edit/IBeatSnapProvider.cs b/osu.Game/Rulesets/Edit/IBeatSnapProvider.cs
index dbad407b75..5e45cefe8c 100644
--- a/osu.Game/Rulesets/Edit/IBeatSnapProvider.cs
+++ b/osu.Game/Rulesets/Edit/IBeatSnapProvider.cs
@@ -1,14 +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
-
namespace osu.Game.Rulesets.Edit
{
public interface IBeatSnapProvider
{
///
- /// Snaps a duration to the closest beat of a timing point applicable at the reference time.
+ /// Snaps a duration to the closest beat of a timing point applicable at the reference time, factoring in the current .
///
/// The time to snap.
/// An optional reference point to use for timing point lookup.
@@ -16,10 +14,10 @@ namespace osu.Game.Rulesets.Edit
double SnapTime(double time, double? referenceTime = null);
///
- /// Get the most appropriate beat length at a given time.
+ /// Get the most appropriate beat length at a given time, pre-divided by .
///
/// A reference time used for lookup.
- /// The most appropriate beat length.
+ /// The most appropriate beat length, divided by .
double GetBeatLengthAtTime(double referenceTime);
///
diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
index c4cb41fb6a..7285315c3b 100644
--- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs
+++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
@@ -7,7 +7,6 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods
@@ -71,7 +70,7 @@ namespace osu.Game.Rulesets.Mods
SpeedChange.SetDefault();
double firstObjectStart = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0;
- double lastObjectEnd = beatmap.HitObjects.LastOrDefault()?.GetEndTime() ?? 0;
+ double lastObjectEnd = beatmap.HitObjects.Any() ? beatmap.GetLastObjectTime() : 0;
beginRampTime = firstObjectStart;
finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart);
diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs
index 5c76c43f20..af32c7def3 100644
--- a/osu.Game/Rulesets/Objects/BarLineGenerator.cs
+++ b/osu.Game/Rulesets/Objects/BarLineGenerator.cs
@@ -27,11 +27,8 @@ namespace osu.Game.Rulesets.Objects
if (beatmap.HitObjects.Count == 0)
return;
- HitObject firstObject = beatmap.HitObjects.First();
- HitObject lastObject = beatmap.HitObjects.Last();
-
- double firstHitTime = firstObject.StartTime;
- double lastHitTime = 1 + lastObject.GetEndTime();
+ double firstHitTime = beatmap.HitObjects.First().StartTime;
+ double lastHitTime = 1 + beatmap.GetLastObjectTime();
var timingPoints = beatmap.ControlPointInfo.TimingPoints;
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index d6c151028e..096132d024 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
comboColourBrightness.BindValueChanged(_ => UpdateComboColour());
// Apply transforms
- updateState(State.Value, true);
+ updateStateFromResult();
}
///
@@ -266,12 +266,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
// If not loaded, the state update happens in LoadComplete().
if (IsLoaded)
{
- if (Result.IsHit)
- updateState(ArmedState.Hit, true);
- else if (Result.HasResult)
- updateState(ArmedState.Miss, true);
- else
- updateState(ArmedState.Idle, true);
+ updateStateFromResult();
// Combo colour may have been applied via a bindable flow while no object entry was attached.
// Update here to ensure we're in a good state.
@@ -279,6 +274,16 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
}
+ private void updateStateFromResult()
+ {
+ if (Result.IsHit)
+ updateState(ArmedState.Hit, true);
+ else if (Result.HasResult)
+ updateState(ArmedState.Miss, true);
+ else
+ updateState(ArmedState.Idle, true);
+ }
+
protected sealed override void OnFree(HitObjectLifetimeEntry entry)
{
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependenciesProvidingContainer.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependenciesProvidingContainer.cs
new file mode 100644
index 0000000000..6c213497dd
--- /dev/null
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependenciesProvidingContainer.cs
@@ -0,0 +1,36 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+
+namespace osu.Game.Rulesets.UI
+{
+ public partial class DrawableRulesetDependenciesProvidingContainer : Container
+ {
+ private readonly Ruleset ruleset;
+
+ private DrawableRulesetDependencies rulesetDependencies = null!;
+
+ public DrawableRulesetDependenciesProvidingContainer(Ruleset ruleset)
+ {
+ this.ruleset = ruleset;
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ {
+ return rulesetDependencies = new DrawableRulesetDependencies(ruleset, base.CreateChildDependencies(parent));
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (rulesetDependencies.IsNotNull())
+ rulesetDependencies.Dispose();
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index 859be6e210..a7881678f1 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -93,7 +93,8 @@ namespace osu.Game.Rulesets.UI
public readonly BindableBool DisplayJudgements = new BindableBool(true);
[Resolved(CanBeNull = true)]
- private IReadOnlyList mods { get; set; }
+ [CanBeNull]
+ protected IReadOnlyList Mods { get; private set; }
private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager();
@@ -243,9 +244,9 @@ namespace osu.Game.Rulesets.UI
{
base.Update();
- if (!IsNested && mods != null)
+ if (!IsNested && Mods != null)
{
- foreach (var mod in mods)
+ foreach (var mod in Mods)
{
if (mod is IUpdatableByPlayfield updatable)
updatable.Update(this);
@@ -374,9 +375,9 @@ namespace osu.Game.Rulesets.UI
// If this is the first time this DHO is being used, then apply the DHO mods.
// This is done before Apply() so that the state is updated once when the hitobject is applied.
- if (mods != null)
+ if (Mods != null)
{
- foreach (var m in mods.OfType())
+ foreach (var m in Mods.OfType())
m.ApplyToDrawableHitObject(dho);
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
index 52853d3979..4c7564b791 100644
--- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
break;
}
- double lastObjectTime = Objects.LastOrDefault()?.GetEndTime() ?? double.MaxValue;
+ double lastObjectTime = Beatmap.HitObjects.Any() ? Beatmap.GetLastObjectTime() : double.MaxValue;
double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH;
if (RelativeScaleBeatLengths)
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index f955ae9cd6..713625c15f 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -20,6 +20,7 @@ using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osuTK;
using osuTK.Input;
@@ -57,7 +58,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
TernaryStates = CreateTernaryButtons().ToArray();
- AddInternal(placementBlueprintContainer);
+ AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset)
+ {
+ Child = placementBlueprintContainer
+ });
}
protected override void LoadComplete()
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index 3e49c31b1e..03e67306df 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -18,6 +18,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
+using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -54,6 +55,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private ISkinSource skin { get; set; } = null!;
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
public TimelineHitObjectBlueprint(HitObject item)
: base(item)
{
@@ -165,7 +169,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
break;
default:
- return;
+ colour = colourProvider.Highlight1;
+ break;
}
if (IsSelected)
@@ -419,9 +424,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
break;
case IHasDuration endTimeHitObject:
- double snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time));
+ double snappedTime = Math.Max(hitObject.StartTime + beatSnapProvider.GetBeatLengthAtTime(hitObject.StartTime), beatSnapProvider.SnapTime(time));
- if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, beatmap.GetBeatLengthAtTime(snappedTime)))
+ if (endTimeHitObject.EndTime == snappedTime)
return;
endTimeHitObject.Duration = snappedTime - hitObject.StartTime;
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 02130b9662..f3f2b8ad6b 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -40,7 +40,6 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.OSD;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
@@ -538,12 +537,14 @@ namespace osu.Game.Screens.Edit
// Seek to last object time, or track end if already there.
// Note that in osu-stable subsequent presses when at track end won't return to last object.
// This has intentionally been changed to make it more useful.
- double? lastObjectTime = editorBeatmap.HitObjects.LastOrDefault()?.GetEndTime();
-
- if (lastObjectTime == null || clock.CurrentTime == lastObjectTime)
+ if (!editorBeatmap.HitObjects.Any())
+ {
clock.Seek(clock.TrackLength);
- else
- clock.Seek(lastObjectTime.Value);
+ return true;
+ }
+
+ double lastObjectTime = editorBeatmap.GetLastObjectTime();
+ clock.Seek(clock.CurrentTime == lastObjectTime ? clock.TrackLength : lastObjectTime);
return true;
}
diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs
index 36186353f8..64713c7714 100644
--- a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs
+++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs
@@ -94,7 +94,20 @@ namespace osu.Game.Screens.Edit.Timing
try
{
- slider.Current.Parse(t.Text);
+ switch (slider.Current)
+ {
+ case Bindable bindableInt:
+ bindableInt.Value = int.Parse(t.Text);
+ break;
+
+ case Bindable bindableDouble:
+ bindableDouble.Value = double.Parse(t.Text);
+ break;
+
+ default:
+ slider.Current.Parse(t.Text);
+ break;
+ }
}
catch
{
diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
index e1a5c3b23c..65c5128438 100644
--- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
+++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
@@ -58,7 +58,20 @@ namespace osu.Game.Screens.Edit.Timing
try
{
- slider.Current.Parse(t.Text);
+ switch (slider.Current)
+ {
+ case Bindable bindableInt:
+ bindableInt.Value = int.Parse(t.Text);
+ break;
+
+ case Bindable bindableDouble:
+ bindableDouble.Value = double.Parse(t.Text);
+ break;
+
+ default:
+ slider.Current.Parse(t.Text);
+ break;
+ }
}
catch
{
diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs
index 4395b96139..c5ef6b1585 100644
--- a/osu.Game/Screens/Play/ReplayPlayer.cs
+++ b/osu.Game/Screens/Play/ReplayPlayer.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Framework.Input.Bindings;
@@ -13,7 +12,6 @@ using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
@@ -94,7 +92,7 @@ namespace osu.Game.Screens.Play
void keyboardSeek(int direction)
{
- double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.HitObjects.Last().GetEndTime());
+ double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.GetLastObjectTime());
Seek(target);
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 6e75450594..a176d73854 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -35,7 +35,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index bb20b0474d..e192467247 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -62,7 +62,7 @@
-
+
@@ -82,7 +82,7 @@
-
+