1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 17:43:05 +08:00

Merge branch 'master' into fix-exported-replay-overwrite

This commit is contained in:
Bartłomiej Dach 2022-12-03 21:09:19 +01:00
commit cb64919947
No known key found for this signature in database
42 changed files with 484 additions and 92 deletions

View File

@ -52,7 +52,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1127.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.1127.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1130.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.1204.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -4,8 +4,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; 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<DrawableCatchRuleset>().Single());
return this.ChildrenOfType<MenuCursorContainer>().Single().State.Value == Visibility.Hidden;
}
});
}
private bool passCondition() private bool passCondition()
{ {
var playfield = this.ChildrenOfType<CatchPlayfield>().Single(); var playfield = this.ChildrenOfType<CatchPlayfield>().Single();

View File

@ -3,13 +3,16 @@
#nullable disable #nullable disable
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osuTK; using osuTK;
@ -49,6 +52,14 @@ namespace osu.Game.Rulesets.Catch.UI
this.difficulty = difficulty; this.difficulty = difficulty;
} }
protected override GameplayCursorContainer CreateCursor()
{
if (Mods != null && Mods.Any(m => m is ModRelax))
return new CatchRelaxCursorContainer();
return base.CreateCursor();
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {

View File

@ -0,0 +1,15 @@
// 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 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();
}
}

View File

@ -90,6 +90,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public override bool CursorInPlacementArea => false; public override bool CursorInPlacementArea => false;
public TestHitObjectComposer(Playfield playfield) public TestHitObjectComposer(Playfield playfield)
: base(new ManiaRuleset())
{ {
Playfield = playfield; Playfield = playfield;
} }

View File

@ -4,6 +4,8 @@
#nullable disable #nullable disable
using System; using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -22,6 +24,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
private bool isPlacingEnd; private bool isPlacingEnd;
[Resolved(CanBeNull = true)]
[CanBeNull]
private IBeatSnapProvider beatSnapProvider { get; set; }
public SpinnerPlacementBlueprint() public SpinnerPlacementBlueprint()
: base(new Spinner { Position = OsuPlayfield.BASE_SIZE / 2 }) : base(new Spinner { Position = OsuPlayfield.BASE_SIZE / 2 })
{ {
@ -33,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
base.Update(); base.Update();
if (isPlacingEnd) if (isPlacingEnd)
HitObject.EndTime = Math.Max(HitObject.StartTime, EditorClock.CurrentTime); updateEndTimeFromCurrent();
piece.UpdateFrom(HitObject); piece.UpdateFrom(HitObject);
} }
@ -45,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
if (e.Button != MouseButton.Right) if (e.Button != MouseButton.Right)
return false; return false;
HitObject.EndTime = EditorClock.CurrentTime; updateEndTimeFromCurrent();
EndPlacement(true); EndPlacement(true);
} }
else else
@ -61,5 +67,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
return true; 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));
}
} }
} }

View File

@ -17,7 +17,6 @@ using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
@ -196,8 +195,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private IEnumerable<double> generateBeats(IBeatmap beatmap, IReadOnlyCollection<OsuHitObject> originalHitObjects) private IEnumerable<double> generateBeats(IBeatmap beatmap, IReadOnlyCollection<OsuHitObject> originalHitObjects)
{ {
double startTime = originalHitObjects.First().StartTime; double startTime = beatmap.HitObjects.First().StartTime;
double endTime = originalHitObjects.Last().GetEndTime(); double endTime = beatmap.GetLastObjectTime();
var beats = beatmap.ControlPointInfo.TimingPoints var beats = beatmap.ControlPointInfo.TimingPoints
// Ignore timing points after endTime // Ignore timing points after endTime

View File

@ -0,0 +1,37 @@
// 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.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();
}
}
}

View File

@ -0,0 +1,65 @@
// 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.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);
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{ {
private Sprite kiai = null!; private Sprite kiai = null!;
private bool kiaiDisplayed; private bool isKiaiActive;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) 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) protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{ {
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); isKiaiActive = effectPoint.KiaiMode;
if (effectPoint.KiaiMode != kiaiDisplayed)
{
kiaiDisplayed = effectPoint.KiaiMode;
kiai.ClearTransforms();
kiai.FadeTo(kiaiDisplayed ? 1 : 0, 200);
}
} }
} }
} }

View File

@ -129,6 +129,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.Mascot: case TaikoSkinComponents.Mascot:
return new DrawableTaikoMascot(); return new DrawableTaikoMascot();
case TaikoSkinComponents.KiaiGlow:
if (GetTexture("taiko-glow") != null)
return new LegacyKiaiGlow();
return null;
default: default:
throw new UnsupportedSkinComponentException(lookup); throw new UnsupportedSkinComponentException(lookup);
} }

View File

@ -21,5 +21,6 @@ namespace osu.Game.Rulesets.Taiko
TaikoExplosionKiai, TaikoExplosionKiai,
Scroller, Scroller,
Mascot, Mascot,
KiaiGlow
} }
} }

View File

@ -112,6 +112,10 @@ namespace osu.Game.Rulesets.Taiko.UI
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
Children = new[] Children = new[]
{ {
new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.KiaiGlow), _ => Empty())
{
RelativeSizeAxes = Axes.Both,
},
hitExplosionContainer = new Container<HitExplosion> hitExplosionContainer = new Container<HitExplosion>
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -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] [Test]
public void TestDecodeBeatmapComboOffsetsOsu() public void TestDecodeBeatmapComboOffsetsOsu()
{ {

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -137,6 +138,31 @@ namespace osu.Game.Tests.Gameplay
AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss); 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 private partial class TestDrawableHitObject : DrawableHitObject
{ {
public const double INITIAL_LIFETIME_OFFSET = 100; public const double INITIAL_LIFETIME_OFFSET = 100;

View File

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

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -187,18 +188,22 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestInputDoesntWorkWhenHUDHidden() public void TestInputDoesntWorkWhenHUDHidden()
{ {
SongProgressBar getSongProgress() => hudOverlay.ChildrenOfType<SongProgressBar>().Single(); SongProgressBar? getSongProgress() => hudOverlay.ChildrenOfType<SongProgressBar>().SingleOrDefault();
bool seeked = false; bool seeked = false;
createNew(); createNew();
AddUntilStep("wait for song progress", () => getSongProgress() != null);
AddStep("bind seek", () => AddStep("bind seek", () =>
{ {
seeked = false; seeked = false;
var progress = getSongProgress(); var progress = getSongProgress();
Debug.Assert(progress != null);
progress.ShowHandle = true; progress.ShowHandle = true;
progress.OnSeek += _ => seeked = true; progress.OnSeek += _ => seeked = true;
}); });

View File

@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded); AddUntilStep("wait for load", () => downloadButton.IsLoaded);
AddAssert("state is available", () => downloadButton.State.Value == DownloadState.NotDownloaded); checkState(DownloadState.NotDownloaded);
AddStep("click button", () => AddStep("click button", () =>
{ {
@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded); 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<DownloadButton>().First().Enabled.Value); AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
} }
@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded); 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<DownloadButton>().First().Enabled.Value); AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
} }
@ -174,17 +174,16 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
AddUntilStep("wait for load", () => downloadButton.IsLoaded); AddUntilStep("wait for load", () => downloadButton.IsLoaded);
checkState(DownloadState.NotDownloaded);
AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true))); 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<DownloadButton>().First().Enabled.Value); AddAssert("button is enabled", () => downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
AddStep("delete score", () => scoreManager.Delete(imported.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<DownloadButton>().First().Enabled.Value); AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
} }
@ -202,10 +201,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded); 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<DownloadButton>().First().Enabled.Value); AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().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 private ScoreInfo getScoreInfo(bool replayAvailable, bool hasOnlineId = true) => new ScoreInfo
{ {
OnlineID = hasOnlineId ? online_score_id : 0, OnlineID = hasOnlineId ? online_score_id : 0,

View File

@ -73,6 +73,11 @@ namespace osu.Game.Tests.Visual.Online
messageIdSequence = 0; messageIdSequence = 0;
channelManager.CurrentChannel.Value = testChannel = new Channel(); channelManager.CurrentChannel.Value = testChannel = new Channel();
reinitialiseDrawableDisplay();
});
private void reinitialiseDrawableDisplay()
{
Children = new[] Children = new[]
{ {
chatDisplay = new TestStandAloneChatDisplay chatDisplay = new TestStandAloneChatDisplay
@ -92,13 +97,14 @@ namespace osu.Game.Tests.Visual.Online
Channel = { Value = testChannel }, Channel = { Value = testChannel },
} }
}; };
}); }
[Test] [Test]
public void TestSystemMessageOrdering() public void TestSystemMessageOrdering()
{ {
var standardMessage = new Message(messageIdSequence++) var standardMessage = new Message(messageIdSequence++)
{ {
Timestamp = DateTimeOffset.Now,
Sender = admin, Sender = admin,
Content = "I am a wang!" 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 infoMessage1 = new InfoMessage($"the system is calling {messageIdSequence++}");
var infoMessage2 = 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 admin", () => testChannel.AddNewMessages(standardMessage));
AddStep("message from system", () => testChannel.AddNewMessages(infoMessage1)); AddStep("message from system", () => testChannel.AddNewMessages(infoMessage1));
AddStep("message from system", () => testChannel.AddNewMessages(infoMessage2)); AddStep("message from system", () => testChannel.AddNewMessages(infoMessage2));
AddStep("message from admin", () => testChannel.AddNewMessages(standardMessage2));
AddAssert("message order is correct", () => testChannel.Messages.Count == 3 AddAssert("count is correct", () => testChannel.Messages.Count, () => Is.EqualTo(4));
&& testChannel.Messages[0] == standardMessage
&& testChannel.Messages[1] == infoMessage1 AddAssert("message order is correct", () => testChannel.Messages, () => Is.EqualTo(new[]
&& testChannel.Messages[2] == infoMessage2); {
standardMessage,
infoMessage1,
infoMessage2,
standardMessage2
}));
AddAssert("displayed order is correct", () => chatDisplay.DrawableChannel.ChildrenOfType<ChatLine>().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<ChatLine>().Select(c => c.Message), () => Is.EqualTo(new[]
{
standardMessage,
infoMessage1,
infoMessage2,
standardMessage2
}));
} }
[Test] [Test]

View File

@ -81,9 +81,14 @@ namespace osu.Game.Beatmaps
public double GetMostCommonBeatLength() public double GetMostCommonBeatLength()
{ {
double lastTime;
// The last playable time in the beatmap - the last timing point extends to this time. // 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. // 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 = var mostCommon =
// Construct a set of (beatLength, duration) tuples for each individual timing point. // Construct a set of (beatLength, duration) tuples for each individual timing point.

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -102,5 +103,16 @@ namespace osu.Game.Beatmaps
addCombo(nested, ref combo); addCombo(nested, ref combo);
} }
} }
/// <summary>
/// Find the absolute end time of the latest <see cref="HitObject"/> in a beatmap. Will throw if beatmap contains no objects.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static double GetLastObjectTime(this IBeatmap beatmap) => beatmap.HitObjects.Max(h => h.GetEndTime());
} }
} }

View File

@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.Chat namespace osu.Game.Online.Chat
@ -13,7 +10,6 @@ namespace osu.Game.Online.Chat
public InfoMessage(string message) public InfoMessage(string message)
: base(null) : base(null)
{ {
Timestamp = DateTimeOffset.Now;
Content = message; Content = message;
Sender = APIUser.SYSTEM_USER; Sender = APIUser.SYSTEM_USER;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Online.Chat namespace osu.Game.Online.Chat
{ {
public class LocalEchoMessage : LocalMessage public class LocalEchoMessage : LocalMessage

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable using System;
namespace osu.Game.Online.Chat namespace osu.Game.Online.Chat
{ {
@ -13,6 +13,7 @@ namespace osu.Game.Online.Chat
protected LocalMessage(long? id) protected LocalMessage(long? id)
: base(id) : base(id)
{ {
Timestamp = DateTimeOffset.Now;
} }
} }
} }

View File

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -59,19 +60,28 @@ namespace osu.Game.Online.Chat
/// <remarks>The <see cref="Link"/>s' <see cref="Link.Index"/> and <see cref="Link.Length"/>s are according to <see cref="DisplayContent"/></remarks> /// <remarks>The <see cref="Link"/>s' <see cref="Link.Index"/> and <see cref="Link.Length"/>s are according to <see cref="DisplayContent"/></remarks>
public List<Link> Links; public List<Link> Links;
private static long constructionOrderStatic;
private readonly long constructionOrder;
public Message(long? id) public Message(long? id)
{ {
Id = id; Id = id;
constructionOrder = Interlocked.Increment(ref constructionOrderStatic);
} }
public int CompareTo(Message other) public int CompareTo(Message other)
{ {
if (!Id.HasValue) if (Id.HasValue && other.Id.HasValue)
return other.Id.HasValue ? 1 : Timestamp.CompareTo(other.Timestamp); return Id.Value.CompareTo(other.Id.Value);
if (!other.Id.HasValue)
return -1;
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) public virtual bool Equals(Message other)
@ -85,6 +95,6 @@ namespace osu.Game.Online.Chat
// ReSharper disable once ImpureMethodCallOnReadonlyValueField // ReSharper disable once ImpureMethodCallOnReadonlyValueField
public override int GetHashCode() => Id.GetHashCode(); public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => $"[{ChannelId}] ({Id}) {Sender}: {Content}"; public override string ToString() => $"({(Id?.ToString() ?? "null")}) {Timestamp} {Sender}: {Content}";
} }
} }

View File

@ -81,7 +81,7 @@ namespace osu.Game.Overlays.FirstRunSetup
loading.Hide(); loading.Hide();
tick.FadeIn(500, Easing.OutQuint); 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; progressBar.FillColour = colours.Green;
this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint); this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint);

View File

@ -45,8 +45,6 @@ namespace osu.Game.Rulesets.Edit
{ {
protected IRulesetConfigManager Config { get; private set; } protected IRulesetConfigManager Config { get; private set; }
protected readonly Ruleset Ruleset;
// Provides `Playfield` // Provides `Playfield`
private DependencyContainer dependencies; private DependencyContainer dependencies;
@ -74,8 +72,8 @@ namespace osu.Game.Rulesets.Edit
private IBindable<bool> hasTiming; private IBindable<bool> hasTiming;
protected HitObjectComposer(Ruleset ruleset) protected HitObjectComposer(Ruleset ruleset)
: base(ruleset)
{ {
Ruleset = ruleset;
} }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
@ -419,8 +417,11 @@ namespace osu.Game.Rulesets.Edit
[Cached] [Cached]
public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
{ {
protected HitObjectComposer() public readonly Ruleset Ruleset;
protected HitObjectComposer(Ruleset ruleset)
{ {
Ruleset = ruleset;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }

View File

@ -1,14 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
{ {
public interface IBeatSnapProvider public interface IBeatSnapProvider
{ {
/// <summary> /// <summary>
/// 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 <see cref="BeatDivisor"/>.
/// </summary> /// </summary>
/// <param name="time">The time to snap.</param> /// <param name="time">The time to snap.</param>
/// <param name="referenceTime">An optional reference point to use for timing point lookup.</param> /// <param name="referenceTime">An optional reference point to use for timing point lookup.</param>
@ -16,10 +14,10 @@ namespace osu.Game.Rulesets.Edit
double SnapTime(double time, double? referenceTime = null); double SnapTime(double time, double? referenceTime = null);
/// <summary> /// <summary>
/// Get the most appropriate beat length at a given time. /// Get the most appropriate beat length at a given time, pre-divided by <see cref="BeatDivisor"/>.
/// </summary> /// </summary>
/// <param name="referenceTime">A reference time used for lookup.</param> /// <param name="referenceTime">A reference time used for lookup.</param>
/// <returns>The most appropriate beat length.</returns> /// <returns>The most appropriate beat length, divided by <see cref="BeatDivisor"/>.</returns>
double GetBeatLengthAtTime(double referenceTime); double GetBeatLengthAtTime(double referenceTime);
/// <summary> /// <summary>

View File

@ -7,7 +7,6 @@ using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
@ -71,7 +70,7 @@ namespace osu.Game.Rulesets.Mods
SpeedChange.SetDefault(); SpeedChange.SetDefault();
double firstObjectStart = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0; double firstObjectStart = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0;
double lastObjectEnd = beatmap.HitObjects.LastOrDefault()?.GetEndTime() ?? 0; double lastObjectEnd = beatmap.HitObjects.Any() ? beatmap.GetLastObjectTime() : 0;
beginRampTime = firstObjectStart; beginRampTime = firstObjectStart;
finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart); finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart);

View File

@ -27,11 +27,8 @@ namespace osu.Game.Rulesets.Objects
if (beatmap.HitObjects.Count == 0) if (beatmap.HitObjects.Count == 0)
return; return;
HitObject firstObject = beatmap.HitObjects.First(); double firstHitTime = beatmap.HitObjects.First().StartTime;
HitObject lastObject = beatmap.HitObjects.Last(); double lastHitTime = 1 + beatmap.GetLastObjectTime();
double firstHitTime = firstObject.StartTime;
double lastHitTime = 1 + lastObject.GetEndTime();
var timingPoints = beatmap.ControlPointInfo.TimingPoints; var timingPoints = beatmap.ControlPointInfo.TimingPoints;

View File

@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
comboColourBrightness.BindValueChanged(_ => UpdateComboColour()); comboColourBrightness.BindValueChanged(_ => UpdateComboColour());
// Apply transforms // Apply transforms
updateState(State.Value, true); updateStateFromResult();
} }
/// <summary> /// <summary>
@ -266,12 +266,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
// If not loaded, the state update happens in LoadComplete(). // If not loaded, the state update happens in LoadComplete().
if (IsLoaded) if (IsLoaded)
{ {
if (Result.IsHit) updateStateFromResult();
updateState(ArmedState.Hit, true);
else if (Result.HasResult)
updateState(ArmedState.Miss, true);
else
updateState(ArmedState.Idle, true);
// Combo colour may have been applied via a bindable flow while no object entry was attached. // 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. // 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) protected sealed override void OnFree(HitObjectLifetimeEntry entry)
{ {
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);

View File

@ -0,0 +1,36 @@
// 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 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();
}
}
}

View File

@ -93,7 +93,8 @@ namespace osu.Game.Rulesets.UI
public readonly BindableBool DisplayJudgements = new BindableBool(true); public readonly BindableBool DisplayJudgements = new BindableBool(true);
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IReadOnlyList<Mod> mods { get; set; } [CanBeNull]
protected IReadOnlyList<Mod> Mods { get; private set; }
private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager(); private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager();
@ -243,9 +244,9 @@ namespace osu.Game.Rulesets.UI
{ {
base.Update(); 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) if (mod is IUpdatableByPlayfield updatable)
updatable.Update(this); 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. // 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. // 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<IApplicableToDrawableHitObject>()) foreach (var m in Mods.OfType<IApplicableToDrawableHitObject>())
m.ApplyToDrawableHitObject(dho); m.ApplyToDrawableHitObject(dho);
} }
} }

View File

@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
break; break;
} }
double lastObjectTime = Objects.LastOrDefault()?.GetEndTime() ?? double.MaxValue; double lastObjectTime = Beatmap.HitObjects.Any() ? Beatmap.GetLastObjectTime() : double.MaxValue;
double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH; double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH;
if (RelativeScaleBeatLengths) if (RelativeScaleBeatLengths)

View File

@ -20,6 +20,7 @@ using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Components.TernaryButtons;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -57,7 +58,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
TernaryStates = CreateTernaryButtons().ToArray(); TernaryStates = CreateTernaryButtons().ToArray();
AddInternal(placementBlueprintContainer); AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset)
{
Child = placementBlueprintContainer
});
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -18,6 +18,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -54,6 +55,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private ISkinSource skin { get; set; } = null!; private ISkinSource skin { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public TimelineHitObjectBlueprint(HitObject item) public TimelineHitObjectBlueprint(HitObject item)
: base(item) : base(item)
{ {
@ -165,7 +169,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
break; break;
default: default:
return; colour = colourProvider.Highlight1;
break;
} }
if (IsSelected) if (IsSelected)
@ -419,9 +424,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
break; break;
case IHasDuration endTimeHitObject: 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; return;
endTimeHitObject.Duration = snappedTime - hitObject.StartTime; endTimeHitObject.Duration = snappedTime - hitObject.StartTime;

View File

@ -40,7 +40,6 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.OSD; using osu.Game.Overlays.OSD;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components.Timeline; 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. // 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. // 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. // This has intentionally been changed to make it more useful.
double? lastObjectTime = editorBeatmap.HitObjects.LastOrDefault()?.GetEndTime(); if (!editorBeatmap.HitObjects.Any())
{
if (lastObjectTime == null || clock.CurrentTime == lastObjectTime)
clock.Seek(clock.TrackLength); clock.Seek(clock.TrackLength);
else return true;
clock.Seek(lastObjectTime.Value); }
double lastObjectTime = editorBeatmap.GetLastObjectTime();
clock.Seek(clock.CurrentTime == lastObjectTime ? clock.TrackLength : lastObjectTime);
return true; return true;
} }

View File

@ -94,7 +94,20 @@ namespace osu.Game.Screens.Edit.Timing
try try
{ {
slider.Current.Parse(t.Text); switch (slider.Current)
{
case Bindable<int> bindableInt:
bindableInt.Value = int.Parse(t.Text);
break;
case Bindable<double> bindableDouble:
bindableDouble.Value = double.Parse(t.Text);
break;
default:
slider.Current.Parse(t.Text);
break;
}
} }
catch catch
{ {

View File

@ -58,7 +58,20 @@ namespace osu.Game.Screens.Edit.Timing
try try
{ {
slider.Current.Parse(t.Text); switch (slider.Current)
{
case Bindable<int> bindableInt:
bindableInt.Value = int.Parse(t.Text);
break;
case Bindable<double> bindableDouble:
bindableDouble.Value = double.Parse(t.Text);
break;
default:
slider.Current.Parse(t.Text);
break;
}
} }
catch catch
{ {

View File

@ -5,7 +5,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
@ -13,7 +12,6 @@ using osu.Framework.Input.Events;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -94,7 +92,7 @@ namespace osu.Game.Screens.Play
void keyboardSeek(int direction) 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); Seek(target);
} }

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.18.0" /> <PackageReference Include="Realm" Version="10.18.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.1130.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.1204.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1127.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.1127.0" />
<PackageReference Include="Sentry" Version="3.23.1" /> <PackageReference Include="Sentry" Version="3.23.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -62,7 +62,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1127.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.1127.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.1130.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.1204.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
<PropertyGroup> <PropertyGroup>
@ -82,7 +82,7 @@
<PackageReference Include="DiffPlex" Version="1.7.1" /> <PackageReference Include="DiffPlex" Version="1.7.1" />
<PackageReference Include="Humanizer" Version="2.14.1" /> <PackageReference Include="Humanizer" Version="2.14.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.1130.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.1204.0" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />