diff --git a/osu.Android.props b/osu.Android.props
index 651e5b1fe6..d64855e5c1 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -10,7 +10,7 @@
true
-
+
Release Difference / ms
// release_threshold
if (isOverlapping)
- holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime)));
+ holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime)));
// Decay and increase individualStrains in own column
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs
index c9ee5af809..44120e16e6 100644
--- a/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs
@@ -33,5 +33,6 @@ namespace osu.Game.Rulesets.Mania
HitExplosion,
StageBackground,
StageForeground,
+ BarLine
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
index 2b0098744f..4e6cc4f1d6 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
@@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) };
- public const double END_NOTE_ALLOW_THRESHOLD = 0.5;
-
public void ApplyToBeatmap(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
@@ -46,28 +44,9 @@ namespace osu.Game.Rulesets.Mania.Mods
StartTime = h.StartTime,
Samples = h.GetNodeSamples(0)
});
-
- // Don't add an end note if the duration is shorter than the threshold
- double noteValue = GetNoteDurationInBeatLength(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc.
-
- if (noteValue >= END_NOTE_ALLOW_THRESHOLD)
- {
- newObjects.Add(new Note
- {
- Column = h.Column,
- StartTime = h.EndTime,
- Samples = h.GetNodeSamples((h.NodeSamples?.Count - 1) ?? 1)
- });
- }
}
maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType().Concat(newObjects).OrderBy(h => h.StartTime).ToList();
}
-
- public static double GetNoteDurationInBeatLength(HoldNote holdNote, ManiaBeatmap beatmap)
- {
- double beatLength = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BeatLength;
- return holdNote.Duration / beatLength;
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/BarLine.cs b/osu.Game.Rulesets.Mania/Objects/BarLine.cs
index 09a746042b..cf576239ed 100644
--- a/osu.Game.Rulesets.Mania/Objects/BarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/BarLine.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 osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@@ -8,7 +9,15 @@ namespace osu.Game.Rulesets.Mania.Objects
{
public class BarLine : ManiaHitObject, IBarLine
{
- public bool Major { get; set; }
+ private HitObjectProperty major;
+
+ public Bindable MajorBindable => major.Bindable;
+
+ public bool Major
+ {
+ get => major.Value;
+ set => major.Value = value;
+ }
public override Judgement CreateJudgement() => new IgnoreJudgement();
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
index 8381b8b24b..25fed1a84c 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
@@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Shapes;
-using osuTK;
+using osu.Game.Rulesets.Mania.Skinning.Default;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@@ -13,45 +15,41 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public partial class DrawableBarLine : DrawableManiaHitObject
{
+ public readonly Bindable Major = new Bindable();
+
+ public DrawableBarLine()
+ : this(null!)
+ {
+ }
+
public DrawableBarLine(BarLine barLine)
: base(barLine)
{
RelativeSizeAxes = Axes.X;
- Height = barLine.Major ? 1.7f : 1.2f;
+ }
- AddInternal(new Box
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.BarLine), _ => new DefaultBarLine())
{
- Name = "Bar line",
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- RelativeSizeAxes = Axes.Both,
- Alpha = barLine.Major ? 0.5f : 0.2f
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
});
- if (barLine.Major)
- {
- Vector2 size = new Vector2(22, 6);
- const float line_offset = 4;
+ Major.BindValueChanged(major => Height = major.NewValue ? 1.7f : 1.2f, true);
+ }
- AddInternal(new Circle
- {
- Name = "Left line",
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreRight,
+ protected override void OnApply()
+ {
+ base.OnApply();
+ Major.BindTo(HitObject.MajorBindable);
+ }
- Size = size,
- X = -line_offset,
- });
-
- AddInternal(new Circle
- {
- Name = "Right line",
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreLeft,
- Size = size,
- X = line_offset,
- });
- }
+ protected override void OnFree()
+ {
+ base.OnFree();
+ Major.UnbindFrom(HitObject.MajorBindable);
}
protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150);
diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs
new file mode 100644
index 0000000000..cd85901a65
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs
@@ -0,0 +1,72 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Default
+{
+ public partial class DefaultBarLine : CompositeDrawable
+ {
+ private Bindable major = null!;
+
+ private Drawable mainLine = null!;
+ private Drawable leftAnchor = null!;
+ private Drawable rightAnchor = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ AddInternal(mainLine = new Box
+ {
+ Name = "Bar line",
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.Both,
+ });
+
+ Vector2 size = new Vector2(22, 6);
+ const float line_offset = 4;
+
+ AddInternal(leftAnchor = new Circle
+ {
+ Name = "Left anchor",
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreRight,
+ Size = size,
+ X = -line_offset,
+ });
+
+ AddInternal(rightAnchor = new Circle
+ {
+ Name = "Right anchor",
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreLeft,
+ Size = size,
+ X = line_offset,
+ });
+
+ major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ major.BindValueChanged(updateMajor, true);
+ }
+
+ private void updateMajor(ValueChangedEvent major)
+ {
+ mainLine.Alpha = major.NewValue ? 0.5f : 0.2f;
+ leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? 1 : 0;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index f8519beb22..446dfae0f6 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -119,6 +119,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
case ManiaSkinComponents.StageForeground:
return new LegacyStageForeground();
+ case ManiaSkinComponents.BarLine:
+ return null; // Not yet implemented.
+
default:
throw new UnsupportedSkinComponentException(lookup);
}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 7b00447238..314d199944 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -30,15 +30,15 @@ namespace osu.Game.Rulesets.Mania.UI
{
get
{
- if (Stages.Count == 1)
- return Stages.First().ScreenSpaceDrawQuad;
+ RectangleF totalArea = RectangleF.Empty;
- RectangleF area = RectangleF.Empty;
+ for (int i = 0; i < Stages.Count; ++i)
+ {
+ var stageArea = Stages[i].ScreenSpaceDrawQuad.AABBFloat;
+ totalArea = i == 0 ? stageArea : RectangleF.Union(totalArea, stageArea);
+ }
- foreach (var stage in Stages)
- area = RectangleF.Union(area, stage.ScreenSpaceDrawQuad.AABBFloat);
-
- return area;
+ return totalArea;
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index 879c704450..4382f8e84a 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -136,6 +136,8 @@ namespace osu.Game.Rulesets.Mania.UI
columnFlow.SetContentForColumn(i, column);
AddNested(column);
}
+
+ RegisterPool(50, 200);
}
private ISkinSource currentSkin;
@@ -186,7 +188,7 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h);
- public void Add(BarLine barLine) => base.Add(new DrawableBarLine(barLine));
+ public void Add(BarLine barLine) => base.Add(barLine);
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index dddd1e3c5a..5f3d0f898e 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -139,7 +139,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = taikoDuration,
- SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
};
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index 083b8cc547..5f47d486e6 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -3,7 +3,6 @@
using osu.Game.Rulesets.Objects.Types;
using System.Threading;
-using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
@@ -14,7 +13,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects
{
- public class DrumRoll : TaikoStrongableHitObject, IHasPath, IHasSliderVelocity
+ public class DrumRoll : TaikoStrongableHitObject, IHasPath
{
///
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@@ -34,19 +33,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
///
public double Velocity { get; private set; }
- public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1)
- {
- Precision = 0.01,
- MinValue = 0.1,
- MaxValue = 10
- };
-
- public double SliderVelocity
- {
- get => SliderVelocityBindable.Value;
- set => SliderVelocityBindable.Value = value;
- }
-
///
/// Numer of ticks per beat length.
///
@@ -63,8 +49,9 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
+ EffectControlPoint effectPoint = controlPointInfo.EffectPointAt(StartTime);
- double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
+ double scoringDistance = base_distance * difficulty.SliderMultiplier * effectPoint.ScrollSpeed;
Velocity = scoringDistance / timingPoint.BeatLength;
TickRate = difficulty.SliderTickRate == 3 ? 3 : 4;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index 37cb43a43a..920e560018 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -92,25 +92,6 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
- [FlakyTest]
- /*
- * Fail rate around 1.2%.
- *
- * Failing with realm refetch occasionally being null.
- * My only guess is that the WorkingBeatmap at SetupScreen is dummy instead of the true one.
- * If it's something else, we have larger issues with realm, but I don't think that's the case.
- *
- * at osu.Framework.Logging.ThrowingTraceListener.Fail(String message1, String message2)
- * at System.Diagnostics.TraceInternal.Fail(String message, String detailMessage)
- * at System.Diagnostics.TraceInternal.TraceProvider.Fail(String message, String detailMessage)
- * at System.Diagnostics.Debug.Fail(String message, String detailMessage)
- * at osu.Game.Database.ModelManager`1.<>c__DisplayClass8_0.b__0(Realm realm) ModelManager.cs:line 50
- * at osu.Game.Database.RealmExtensions.Write(Realm realm, Action`1 function) RealmExtensions.cs:line 14
- * at osu.Game.Database.ModelManager`1.performFileOperation(TModel item, Action`1 operation) ModelManager.cs:line 47
- * at osu.Game.Database.ModelManager`1.AddFile(TModel item, Stream contents, String filename) ModelManager.cs:line 37
- * at osu.Game.Screens.Edit.Setup.ResourcesSection.ChangeAudioTrack(FileInfo source) ResourcesSection.cs:line 115
- * at osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.b__11_0() TestSceneEditorBeatmapCreation.cs:line 101
- */
public void TestAddAudioTrack()
{
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
index dfa9fdf03b..4a2794c14f 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
@@ -185,6 +185,34 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
}
+ [Test]
+ public void TestGetSegmentEnds()
+ {
+ var positions = new[]
+ {
+ Vector2.Zero,
+ new Vector2(100, 0),
+ new Vector2(100),
+ new Vector2(200, 100),
+ };
+ double[] distances = { 100d, 200d, 300d };
+
+ AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.Linear))));
+ AddAssert("segment ends are correct", () => path.GetSegmentEnds().SequenceEqual(distances.Select(d => d / 300)));
+ AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)).SequenceEqual(positions.Skip(1)));
+ AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 400);
+ AddAssert("segment ends are correct", () => path.GetSegmentEnds().SequenceEqual(distances.Select(d => d / 400)));
+ AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)).SequenceEqual(positions.Skip(1)));
+ AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
+ AddAssert("segment ends are correct", () => path.GetSegmentEnds().SequenceEqual(distances.Select(d => d / 150)));
+ AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)).SequenceEqual(new[]
+ {
+ positions[1],
+ new Vector2(100, 50),
+ new Vector2(100, 50),
+ }));
+ }
+
private List createSegment(PathType type, params Vector2[] controlPoints)
{
var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList();
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs
index 979cb4424e..d1a914300f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs
@@ -84,12 +84,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
- public void TestFocusOnTabKeyWhenExpanded()
+ public void TestFocusOnEnterKeyWhenExpanded()
{
setLocalUserPlaying(true);
assertChatFocused(false);
- AddStep("press tab", () => InputManager.Key(Key.Tab));
+ AddStep("press enter", () => InputManager.Key(Key.Enter));
assertChatFocused(true);
}
@@ -99,19 +99,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
setLocalUserPlaying(true);
assertChatFocused(false);
- AddStep("press tab", () => InputManager.Key(Key.Tab));
+ AddStep("press enter", () => InputManager.Key(Key.Enter));
assertChatFocused(true);
AddStep("press escape", () => InputManager.Key(Key.Escape));
assertChatFocused(false);
}
[Test]
- public void TestFocusOnTabKeyWhenNotExpanded()
+ public void TestFocusOnEnterKeyWhenNotExpanded()
{
AddStep("set not expanded", () => chatDisplay.Expanded.Value = false);
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
- AddStep("press tab", () => InputManager.Key(Key.Tab));
+ AddStep("press enter", () => InputManager.Key(Key.Enter));
assertChatFocused(true);
AddUntilStep("is visible", () => chatDisplay.IsPresent);
@@ -120,21 +120,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
}
- [Test]
- public void TestFocusToggleViaAction()
- {
- AddStep("set not expanded", () => chatDisplay.Expanded.Value = false);
- AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
-
- AddStep("press tab", () => InputManager.Key(Key.Tab));
- assertChatFocused(true);
- AddUntilStep("is visible", () => chatDisplay.IsPresent);
-
- AddStep("press tab", () => InputManager.Key(Key.Tab));
- assertChatFocused(false);
- AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
- }
-
private void assertChatFocused(bool isFocused) =>
AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
index 049c02ffde..4bf2ebc1a4 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
@@ -49,6 +49,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
{
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
Expanded = { Value = true }
}, Add);
});
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
index a61c3f1234..cebc75f90c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
@@ -79,6 +79,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddWaitStep("wait a bit", 20);
}
+ [TestCase(2)]
+ [TestCase(16)]
+ public void TestTeams(int count)
+ {
+ int[] userIds = getPlayerIds(count);
+
+ start(userIds, teams: true);
+ loadSpectateScreen();
+
+ sendFrames(userIds, 1000);
+ AddWaitStep("wait a bit", 20);
+ }
+
[Test]
public void TestMultipleStartRequests()
{
@@ -450,16 +463,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId);
- private void start(int[] userIds, int? beatmapId = null, APIMod[]? mods = null)
+ private void start(int[] userIds, int? beatmapId = null, APIMod[]? mods = null, bool teams = false)
{
AddStep("start play", () =>
{
- foreach (int id in userIds)
+ for (int i = 0; i < userIds.Length; i++)
{
+ int id = userIds[i];
var user = new MultiplayerRoomUser(id)
{
User = new APIUser { Id = id },
Mods = mods ?? Array.Empty(),
+ MatchState = teams ? new TeamVersusUserState { TeamID = i % 2 } : null,
};
OnlinePlayDependencies.MultiplayerClient.AddUser(user, true);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs
new file mode 100644
index 0000000000..d23fcebae3
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs
@@ -0,0 +1,130 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public partial class TestSceneSliderWithTextBoxInput : OsuManualInputManagerTestScene
+ {
+ private SliderWithTextBoxInput sliderWithTextBoxInput = null!;
+
+ private OsuSliderBar slider => sliderWithTextBoxInput.ChildrenOfType>().Single();
+ private Nub nub => sliderWithTextBoxInput.ChildrenOfType().Single();
+ private OsuTextBox textBox => sliderWithTextBoxInput.ChildrenOfType().Single();
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create slider", () => Child = sliderWithTextBoxInput = new SliderWithTextBoxInput("Test Slider")
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 0.5f,
+ Current = new BindableFloat
+ {
+ MinValue = -5,
+ MaxValue = 5,
+ Precision = 0.2f
+ }
+ });
+ }
+
+ [Test]
+ public void TestNonInstantaneousMode()
+ {
+ AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false);
+
+ AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
+ AddStep("change text", () => textBox.Text = "3");
+ AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero);
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero);
+
+ AddStep("commit text", () => InputManager.Key(Key.Enter));
+ AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
+ AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
+
+ AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub));
+ AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
+ AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft));
+ AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("3"));
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
+
+ AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
+ AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
+ AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
+
+ AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
+ AddStep("set text to invalid", () => textBox.Text = "garbage");
+ AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
+
+ AddStep("commit text", () => InputManager.Key(Key.Enter));
+ AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
+ AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
+
+ AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
+ AddStep("set text to invalid", () => textBox.Text = "garbage");
+ AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
+
+ AddStep("lose focus", () => InputManager.ChangeFocus(null));
+ AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
+ AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
+ }
+
+ [Test]
+ public void TestInstantaneousMode()
+ {
+ AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true);
+
+ AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
+ AddStep("change text", () => textBox.Text = "3");
+ AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
+ AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
+
+ AddStep("commit text", () => InputManager.Key(Key.Enter));
+ AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(3));
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
+
+ AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub));
+ AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
+ AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft));
+ AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
+ AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
+
+ AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
+ AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
+
+ AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
+ AddStep("set text to invalid", () => textBox.Text = "garbage");
+ AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
+
+ AddStep("commit text", () => InputManager.Key(Key.Enter));
+ AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
+ AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
+
+ AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
+ AddStep("set text to invalid", () => textBox.Text = "garbage");
+ AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
+
+ AddStep("lose focus", () => InputManager.ChangeFocus(null));
+ AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
+ AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
+ AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs
index 762cfa2519..0f31192a9c 100644
--- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs
+++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs
@@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Testing;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Tests.Visual;
using osu.Game.Tournament.Components;
@@ -17,11 +18,11 @@ namespace osu.Game.Tournament.Tests.Components
[Cached]
private readonly LadderInfo ladder = new LadderInfo();
- [Test]
- public void TestSongBar()
- {
- SongBar songBar = null!;
+ private SongBar songBar = null!;
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
AddStep("create bar", () => Child = songBar = new SongBar
{
RelativeSizeAxes = Axes.X,
@@ -29,7 +30,11 @@ namespace osu.Game.Tournament.Tests.Components
Origin = Anchor.Centre
});
AddUntilStep("wait for loaded", () => songBar.IsLoaded);
+ }
+ [Test]
+ public void TestSongBar()
+ {
AddStep("set beatmap", () =>
{
var beatmap = CreateAPIBeatmap(Ruleset.Value);
diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs
index fa0cbda16b..3d060600f7 100644
--- a/osu.Game.Tournament/Components/SongBar.cs
+++ b/osu.Game.Tournament/Components/SongBar.cs
@@ -14,7 +14,6 @@ using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Rulesets;
using osu.Game.Screens.Menu;
-using osu.Game.Tournament.Models;
using osuTK;
using osuTK.Graphics;
@@ -22,14 +21,14 @@ namespace osu.Game.Tournament.Components
{
public partial class SongBar : CompositeDrawable
{
- private TournamentBeatmap? beatmap;
+ private IBeatmapInfo? beatmap;
public const float HEIGHT = 145 / 2f;
[Resolved]
private IBindable ruleset { get; set; } = null!;
- public TournamentBeatmap? Beatmap
+ public IBeatmapInfo? Beatmap
{
set
{
@@ -37,7 +36,7 @@ namespace osu.Game.Tournament.Components
return;
beatmap = value;
- update();
+ refreshContent();
}
}
@@ -49,7 +48,7 @@ namespace osu.Game.Tournament.Components
set
{
mods = value;
- update();
+ refreshContent();
}
}
@@ -71,19 +70,25 @@ namespace osu.Game.Tournament.Components
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
+ Masking = true;
+ CornerRadius = 5;
+
InternalChildren = new Drawable[]
{
+ new Box
+ {
+ Colour = colours.Gray3,
+ RelativeSizeAxes = Axes.Both,
+ },
flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- LayoutDuration = 500,
- LayoutEasing = Easing.OutQuint,
Direction = FillDirection.Full,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
@@ -93,7 +98,7 @@ namespace osu.Game.Tournament.Components
Expanded = true;
}
- private void update()
+ private void refreshContent()
{
if (beatmap == null)
{
@@ -229,7 +234,7 @@ namespace osu.Game.Tournament.Components
}
}
},
- new TournamentBeatmapPanel(beatmap)
+ new UnmaskedTournamentBeatmapPanel(beatmap)
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
@@ -272,4 +277,18 @@ namespace osu.Game.Tournament.Components
}
}
}
+
+ internal partial class UnmaskedTournamentBeatmapPanel : TournamentBeatmapPanel
+ {
+ public UnmaskedTournamentBeatmapPanel(IBeatmapInfo? beatmap, string mod = "")
+ : base(beatmap, mod)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Masking = false;
+ }
+ }
}
diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
index ba922c7c7b..4e0adb30ac 100644
--- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
+++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Tournament.Components
{
public partial class TournamentBeatmapPanel : CompositeDrawable
{
- public readonly TournamentBeatmap? Beatmap;
+ public readonly IBeatmapInfo? Beatmap;
private readonly string mod;
@@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Components
private Box flash = null!;
- public TournamentBeatmapPanel(TournamentBeatmap? beatmap, string mod = "")
+ public TournamentBeatmapPanel(IBeatmapInfo? beatmap, string mod = "")
{
Beatmap = beatmap;
this.mod = mod;
@@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Components
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.5f),
- OnlineInfo = Beatmap,
+ OnlineInfo = (Beatmap as IBeatmapSetOnlineInfo),
},
new FillFlowContainer
{
diff --git a/osu.Game.Tournament/IPC/MatchIPCInfo.cs b/osu.Game.Tournament/IPC/MatchIPCInfo.cs
index f57518971f..b4575144e7 100644
--- a/osu.Game.Tournament/IPC/MatchIPCInfo.cs
+++ b/osu.Game.Tournament/IPC/MatchIPCInfo.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Tournament.IPC
public Bindable Mods { get; } = new Bindable();
public Bindable State { get; } = new Bindable();
public Bindable ChatChannel { get; } = new Bindable();
- public BindableInt Score1 { get; } = new BindableInt();
- public BindableInt Score2 { get; } = new BindableInt();
+ public BindableLong Score1 { get; } = new BindableLong();
+ public BindableLong Score2 { get; } = new BindableLong();
}
}
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
index 7ae20acc77..f8de34a511 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
@@ -1,181 +1,19 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Tournament.IPC;
-using osuTK;
namespace osu.Game.Tournament.Screens.Gameplay.Components
{
- // TODO: Update to derive from osu-side class?
- public partial class TournamentMatchScoreDisplay : CompositeDrawable
+ public partial class TournamentMatchScoreDisplay : MatchScoreDisplay
{
- private const float bar_height = 18;
-
- private readonly BindableInt score1 = new BindableInt();
- private readonly BindableInt score2 = new BindableInt();
-
- private readonly MatchScoreCounter score1Text;
- private readonly MatchScoreCounter score2Text;
-
- private readonly MatchScoreDiffCounter scoreDiffText;
-
- private readonly Drawable score1Bar;
- private readonly Drawable score2Bar;
-
- public TournamentMatchScoreDisplay()
- {
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
-
- InternalChildren = new[]
- {
- new Box
- {
- Name = "top bar red (static)",
- RelativeSizeAxes = Axes.X,
- Height = bar_height / 4,
- Width = 0.5f,
- Colour = TournamentGame.COLOUR_RED,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopRight
- },
- new Box
- {
- Name = "top bar blue (static)",
- RelativeSizeAxes = Axes.X,
- Height = bar_height / 4,
- Width = 0.5f,
- Colour = TournamentGame.COLOUR_BLUE,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopLeft
- },
- score1Bar = new Box
- {
- Name = "top bar red",
- RelativeSizeAxes = Axes.X,
- Height = bar_height,
- Width = 0,
- Colour = TournamentGame.COLOUR_RED,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopRight
- },
- score1Text = new MatchScoreCounter
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre
- },
- score2Bar = new Box
- {
- Name = "top bar blue",
- RelativeSizeAxes = Axes.X,
- Height = bar_height,
- Width = 0,
- Colour = TournamentGame.COLOUR_BLUE,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopLeft
- },
- score2Text = new MatchScoreCounter
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre
- },
- scoreDiffText = new MatchScoreDiffCounter
- {
- Anchor = Anchor.TopCentre,
- Margin = new MarginPadding
- {
- Top = bar_height / 4,
- Horizontal = 8
- },
- Alpha = 0
- }
- };
- }
-
[BackgroundDependencyLoader]
private void load(MatchIPCInfo ipc)
{
- score1.BindValueChanged(_ => updateScores());
- score1.BindTo(ipc.Score1);
-
- score2.BindValueChanged(_ => updateScores());
- score2.BindTo(ipc.Score2);
- }
-
- private void updateScores()
- {
- score1Text.Current.Value = score1.Value;
- score2Text.Current.Value = score2.Value;
-
- var winningText = score1.Value > score2.Value ? score1Text : score2Text;
- var losingText = score1.Value <= score2.Value ? score1Text : score2Text;
-
- winningText.Winning = true;
- losingText.Winning = false;
-
- var winningBar = score1.Value > score2.Value ? score1Bar : score2Bar;
- var losingBar = score1.Value <= score2.Value ? score1Bar : score2Bar;
-
- int diff = Math.Max(score1.Value, score2.Value) - Math.Min(score1.Value, score2.Value);
-
- losingBar.ResizeWidthTo(0, 400, Easing.OutQuint);
- winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint);
-
- scoreDiffText.Alpha = diff != 0 ? 1 : 0;
- scoreDiffText.Current.Value = -diff;
- scoreDiffText.Origin = score1.Value > score2.Value ? Anchor.TopLeft : Anchor.TopRight;
- }
-
- protected override void UpdateAfterChildren()
- {
- base.UpdateAfterChildren();
- score1Text.X = -Math.Max(5 + score1Text.DrawWidth / 2, score1Bar.DrawWidth);
- score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth);
- }
-
- private partial class MatchScoreCounter : CommaSeparatedScoreCounter
- {
- private OsuSpriteText displayedSpriteText = null!;
-
- public MatchScoreCounter()
- {
- Margin = new MarginPadding { Top = bar_height, Horizontal = 10 };
- }
-
- public bool Winning
- {
- set => updateFont(value);
- }
-
- protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
- {
- displayedSpriteText = s;
- displayedSpriteText.Spacing = new Vector2(-6);
- updateFont(false);
- });
-
- private void updateFont(bool winning)
- => displayedSpriteText.Font = winning
- ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true)
- : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true);
- }
-
- private partial class MatchScoreDiffCounter : CommaSeparatedScoreCounter
- {
- protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
- {
- s.Spacing = new Vector2(-2);
- s.Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: bar_height, fixedWidth: true);
- });
+ Team1Score.BindTo(ipc.Score1);
+ Team2Score.BindTo(ipc.Score2);
}
}
}
diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatWorkingBeatmap.cs
similarity index 76%
rename from osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
rename to osu.Game/Beatmaps/FlatWorkingBeatmap.cs
index d20baf1edb..c2505ec109 100644
--- a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/FlatWorkingBeatmap.cs
@@ -12,25 +12,26 @@ using osu.Game.Skinning;
namespace osu.Game.Beatmaps
{
///
- /// A which can be constructed directly from a .osu file, providing an implementation for
+ /// A which can be constructed directly from an .osu file (via )
+ /// or an instance (via ,
+ /// providing an implementation for
/// .
///
- public class FlatFileWorkingBeatmap : WorkingBeatmap
+ public class FlatWorkingBeatmap : WorkingBeatmap
{
- private readonly Beatmap beatmap;
+ private readonly IBeatmap beatmap;
- public FlatFileWorkingBeatmap(string file, int? beatmapId = null)
- : this(readFromFile(file), beatmapId)
+ public FlatWorkingBeatmap(string file, int? beatmapId = null)
+ : this(readFromFile(file))
{
+ if (beatmapId.HasValue)
+ beatmap.BeatmapInfo.OnlineID = beatmapId.Value;
}
- private FlatFileWorkingBeatmap(Beatmap beatmap, int? beatmapId = null)
+ public FlatWorkingBeatmap(IBeatmap beatmap)
: base(beatmap.BeatmapInfo, null)
{
this.beatmap = beatmap;
-
- if (beatmapId.HasValue)
- beatmap.BeatmapInfo.OnlineID = beatmapId.Value;
}
private static Beatmap readFromFile(string filename)
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index edcbb94368..921284ad4d 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -237,6 +237,12 @@ namespace osu.Game.Configuration
value: disabledState ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(),
shortcut: LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))
),
+ new TrackedSetting(OsuSetting.GameplayLeaderboard, state => new SettingDescription(
+ rawValue: state,
+ name: GlobalActionKeyBindingStrings.ToggleInGameLeaderboard,
+ value: state ? CommonStrings.Enabled.ToLower() : CommonStrings.Disabled.ToLower(),
+ shortcut: LookupKeyBindings(GlobalAction.ToggleInGameLeaderboard))
+ ),
new TrackedSetting(OsuSetting.HUDVisibilityMode, visibilityMode => new SettingDescription(
rawValue: visibilityMode,
name: GameplaySettingsStrings.HUDVisibilityMode,
diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs
index e054652efa..a874353f73 100644
--- a/osu.Game/Database/LegacyBeatmapExporter.cs
+++ b/osu.Game/Database/LegacyBeatmapExporter.cs
@@ -29,9 +29,9 @@ namespace osu.Game.Database
protected override Stream? GetFileContents(BeatmapSetInfo model, INamedFileUsage file)
{
- bool isBeatmap = model.Beatmaps.Any(o => o.Hash == file.File.Hash);
+ var beatmapInfo = model.Beatmaps.SingleOrDefault(o => o.Hash == file.File.Hash);
- if (!isBeatmap)
+ if (beatmapInfo == null)
return base.GetFileContents(model, file);
// Read the beatmap contents and skin
@@ -43,6 +43,9 @@ namespace osu.Game.Database
using var contentStreamReader = new LineBufferedReader(contentStream);
var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader);
+ var workingBeatmap = new FlatWorkingBeatmap(beatmapContent);
+ var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset);
+
using var skinStream = base.GetFileContents(model, file);
if (skinStream == null)
@@ -56,10 +59,10 @@ namespace osu.Game.Database
// Convert beatmap elements to be compatible with legacy format
// So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves
- foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints)
+ foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints)
controlPoint.Time = Math.Floor(controlPoint.Time);
- foreach (var hitObject in beatmapContent.HitObjects)
+ foreach (var hitObject in playableBeatmap.HitObjects)
{
// Truncate end time before truncating start time because end time is dependent on start time
if (hitObject is IHasDuration hasDuration && hitObject is not IHasPath)
@@ -86,7 +89,7 @@ namespace osu.Game.Database
// Encode to legacy format
var stream = new MemoryStream();
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
- new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
+ new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw);
stream.Seek(0, SeekOrigin.Begin);
diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs
index 47feb8a8f9..39dae61d36 100644
--- a/osu.Game/Database/ModelManager.cs
+++ b/osu.Game/Database/ModelManager.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Database
// (ie. if an async import finished very recently).
Realm.Realm.Write(realm =>
{
- var managed = realm.Find(item.ID);
+ var managed = realm.FindWithRefresh(item.ID);
Debug.Assert(managed != null);
operation(managed);
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index f32b161bb6..917d662255 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -82,8 +82,9 @@ namespace osu.Game.Database
/// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations.
/// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores.
/// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files.
+ /// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding.
///
- private const int schema_version = 32;
+ private const int schema_version = 33;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
@@ -771,6 +772,7 @@ namespace osu.Game.Database
break;
case 8:
+ {
// Ctrl -/+ now adjusts UI scale so let's clear any bindings which overlap these combinations.
// New defaults will be populated by the key store afterwards.
var keyBindings = migration.NewRealm.All();
@@ -784,6 +786,7 @@ namespace osu.Game.Database
migration.NewRealm.Remove(decreaseSpeedBinding);
break;
+ }
case 9:
// Pretty pointless to do this as beatmaps aren't really loaded via realm yet, but oh well.
@@ -838,6 +841,7 @@ namespace osu.Game.Database
break;
case 11:
+ {
string keyBindingClassName = getMappedOrOriginalName(typeof(RealmKeyBinding));
if (!migration.OldRealm.Schema.TryFindObjectSchema(keyBindingClassName, out _))
@@ -864,6 +868,7 @@ namespace osu.Game.Database
}
break;
+ }
case 14:
foreach (var beatmap in migration.NewRealm.All())
@@ -1012,6 +1017,19 @@ namespace osu.Game.Database
break;
}
+
+ case 33:
+ {
+ // Clear default bindings for the chat focus toggle,
+ // as they would conflict with the newly-added leaderboard toggle.
+ var keyBindings = migration.NewRealm.All();
+
+ var toggleChatBind = keyBindings.FirstOrDefault(bind => bind.ActionInt == (int)GlobalAction.ToggleChatFocus);
+ if (toggleChatBind != null && toggleChatBind.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Tab }))
+ migration.NewRealm.Remove(toggleChatBind);
+
+ break;
+ }
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs
index 13c4defb83..c84e1e35b8 100644
--- a/osu.Game/Database/RealmExtensions.cs
+++ b/osu.Game/Database/RealmExtensions.cs
@@ -8,6 +8,34 @@ namespace osu.Game.Database
{
public static class RealmExtensions
{
+ ///
+ /// Performs a .
+ /// If a match was not found, a is performed before trying a second time.
+ /// This ensures that an instance is found even if the realm requested against was not in a consistent state.
+ ///
+ /// The realm to operate on.
+ /// The ID of the entity to find in the realm.
+ /// The type of the entity to find in the realm.
+ ///
+ /// The retrieved entity of type .
+ /// Can be if the entity is still not found by even after a refresh.
+ ///
+ public static T? FindWithRefresh(this Realm realm, Guid id) where T : IRealmObject
+ {
+ var found = realm.Find(id);
+
+ if (found == null)
+ {
+ // It may be that we access this from the update thread before a refresh has taken place.
+ // To ensure that behaviour matches what we'd expect (the object generally *should be* available), force
+ // a refresh to bring in any off-thread changes immediately.
+ realm.Refresh();
+ found = realm.Find(id);
+ }
+
+ return found;
+ }
+
///
/// Perform a write operation against the provided realm instance.
///
diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs
index 509fabec59..9e99cba45c 100644
--- a/osu.Game/Database/RealmLive.cs
+++ b/osu.Game/Database/RealmLive.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Database
///
/// Construct a new instance of live realm data.
///
- /// The realm data.
+ /// The realm data. Must be managed (see ).
/// The realm factory the data was sourced from. May be null for an unmanaged object.
public RealmLive(T data, RealmAccess realm)
: base(data.ID)
@@ -62,7 +62,7 @@ namespace osu.Game.Database
return;
}
- perform(retrieveFromID(r));
+ perform(r.FindWithRefresh(ID)!);
RealmLiveStatistics.USAGE_ASYNC.Value++;
});
}
@@ -84,7 +84,7 @@ namespace osu.Game.Database
return realm.Run(r =>
{
- var returnData = perform(retrieveFromID(r));
+ var returnData = perform(r.FindWithRefresh(ID)!);
RealmLiveStatistics.USAGE_ASYNC.Value++;
if (returnData is RealmObjectBase realmObject && realmObject.IsManaged)
@@ -141,25 +141,10 @@ namespace osu.Game.Database
}
dataIsFromUpdateThread = true;
- data = retrieveFromID(realm.Realm);
+ data = realm.Realm.FindWithRefresh(ID)!;
+
RealmLiveStatistics.USAGE_UPDATE_REFETCH.Value++;
}
-
- private T retrieveFromID(Realm realm)
- {
- var found = realm.Find(ID);
-
- if (found == null)
- {
- // It may be that we access this from the update thread before a refresh has taken place.
- // To ensure that behaviour matches what we'd expect (the object *is* available), force
- // a refresh to bring in any off-thread changes immediately.
- realm.Refresh();
- found = realm.Find(ID)!;
- }
-
- return found;
- }
}
internal static class RealmLiveStatistics
diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs
index 37a4fe77bd..64c70095bf 100644
--- a/osu.Game/Graphics/ParticleSpewer.cs
+++ b/osu.Game/Graphics/ParticleSpewer.cs
@@ -49,6 +49,18 @@ namespace osu.Game.Graphics
this.maxDuration = maxDuration;
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Active.BindValueChanged(active =>
+ {
+ // ensure that particles can be spawned immediately after the spewer becomes active.
+ if (active.NewValue)
+ lastParticleAdded = null;
+ });
+ }
+
protected override void Update()
{
base.Update();
@@ -56,12 +68,8 @@ namespace osu.Game.Graphics
Invalidate(Invalidation.DrawNode);
if (!Active.Value || !CanSpawnParticles)
- {
- lastParticleAdded = null;
return;
- }
- // Always want to spawn the first particle in an activation immediately.
if (lastParticleAdded == null)
{
lastParticleAdded = Time.Current;
diff --git a/osu.Game/Graphics/UserInterface/FPSCounter.cs b/osu.Game/Graphics/UserInterface/FPSCounter.cs
index c1ef573848..000b85b900 100644
--- a/osu.Game/Graphics/UserInterface/FPSCounter.cs
+++ b/osu.Game/Graphics/UserInterface/FPSCounter.cs
@@ -213,7 +213,7 @@ namespace osu.Game.Graphics.UserInterface
requestDisplay();
else if (isDisplayed && Time.Current - lastDisplayRequiredTime > 2000 && !IsHovered)
{
- mainContent.FadeTo(0, 300, Easing.OutQuint);
+ mainContent.FadeTo(0.7f, 300, Easing.OutQuint);
isDisplayed = false;
}
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs
new file mode 100644
index 0000000000..fc0e4d2083
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs
@@ -0,0 +1,143 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Globalization;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Localisation;
+using osu.Game.Overlays.Settings;
+using osu.Game.Utils;
+using osuTK;
+
+namespace osu.Game.Graphics.UserInterfaceV2
+{
+ public partial class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue
+ where T : struct, IEquatable, IComparable, IConvertible
+ {
+ ///
+ /// A custom step value for each key press which actuates a change on this control.
+ ///
+ public float KeyboardStep
+ {
+ get => slider.KeyboardStep;
+ set => slider.KeyboardStep = value;
+ }
+
+ public Bindable Current
+ {
+ get => slider.Current;
+ set => slider.Current = value;
+ }
+
+ private bool instantaneous;
+
+ ///
+ /// Whether changes to the slider should instantaneously transfer to the text box (and vice versa).
+ /// If , the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end.
+ ///
+ public bool Instantaneous
+ {
+ get => instantaneous;
+ set
+ {
+ instantaneous = value;
+ slider.TransferValueOnCommit = !instantaneous;
+ }
+ }
+
+ private readonly SettingsSlider slider;
+ private readonly LabelledTextBox textBox;
+
+ public SliderWithTextBoxInput(LocalisableString labelText)
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ InternalChildren = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(20),
+ Children = new Drawable[]
+ {
+ textBox = new LabelledTextBox
+ {
+ Label = labelText,
+ },
+ slider = new SettingsSlider
+ {
+ TransferValueOnCommit = true,
+ RelativeSizeAxes = Axes.X,
+ }
+ }
+ },
+ };
+
+ textBox.OnCommit += textCommitted;
+ textBox.Current.BindValueChanged(textChanged);
+
+ Current.BindValueChanged(updateTextBoxFromSlider, true);
+ }
+
+ private bool updatingFromTextBox;
+
+ private void textChanged(ValueChangedEvent change)
+ {
+ if (!instantaneous) return;
+
+ tryUpdateSliderFromTextBox();
+ }
+
+ private void textCommitted(TextBox t, bool isNew)
+ {
+ tryUpdateSliderFromTextBox();
+
+ // If the attempted update above failed, restore text box to match the slider.
+ Current.TriggerChange();
+ }
+
+ private void tryUpdateSliderFromTextBox()
+ {
+ updatingFromTextBox = true;
+
+ try
+ {
+ switch (slider.Current)
+ {
+ case Bindable bindableInt:
+ bindableInt.Value = int.Parse(textBox.Current.Value);
+ break;
+
+ case Bindable bindableDouble:
+ bindableDouble.Value = double.Parse(textBox.Current.Value);
+ break;
+
+ default:
+ slider.Current.Parse(textBox.Current.Value);
+ break;
+ }
+ }
+ catch
+ {
+ // ignore parsing failures.
+ // sane state will eventually be restored by a commit (either explicit, or implicit via focus loss).
+ }
+
+ updatingFromTextBox = false;
+ }
+
+ private void updateTextBoxFromSlider(ValueChangedEvent _)
+ {
+ if (updatingFromTextBox) return;
+
+ decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
+ textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
+ }
+ }
+}
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 01c454e3f9..1090eeb462 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -116,9 +116,10 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface),
+ new KeyBinding(InputKey.Tab, GlobalAction.ToggleInGameLeaderboard),
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
- new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus),
+ new KeyBinding(InputKey.Enter, GlobalAction.ToggleChatFocus),
new KeyBinding(InputKey.F1, GlobalAction.SaveReplay),
new KeyBinding(InputKey.F2, GlobalAction.ExportReplay),
};
@@ -204,7 +205,6 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleMute))]
ToggleMute,
- // In-Game Keybindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SkipCutscene))]
SkipCutscene,
@@ -232,7 +232,6 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.QuickExit))]
QuickExit,
- // Game-wide beatmap music controller keybindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.MusicNext))]
MusicNext,
@@ -260,7 +259,6 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.PauseGameplay))]
PauseGameplay,
- // Editor
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSetupMode))]
EditorSetupMode,
@@ -285,7 +283,6 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameInterface))]
ToggleInGameInterface,
- // Song select keybindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleModSelection))]
ToggleModSelection,
@@ -378,5 +375,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleReplaySettings))]
ToggleReplaySettings,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameLeaderboard))]
+ ToggleInGameLeaderboard,
}
}
diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
index f93d86225c..ceefc27968 100644
--- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
+++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
@@ -219,6 +219,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString ToggleInGameInterface => new TranslatableString(getKey(@"toggle_in_game_interface"), @"Toggle in-game interface");
+ ///
+ /// "Toggle in-game leaderboard"
+ ///
+ public static LocalisableString ToggleInGameLeaderboard => new TranslatableString(getKey(@"toggle_in_game_leaderboard"), @"Toggle in-game leaderboard");
+
///
/// "Toggle mod select"
///
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 01169828b0..2764247f5c 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Online.API
public Bindable LocalUser { get; } = new Bindable(new APIUser
{
- Username = @"Dummy",
+ Username = @"Local user",
Id = DUMMY_USER_ID,
});
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
index 5a01faa417..f62eeab5d7 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
@@ -104,9 +104,11 @@ namespace osu.Game.Rulesets.Difficulty
public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
{
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
- LegacyAccuracyScore = (int)values[ATTRIB_ID_LEGACY_ACCURACY_SCORE];
- LegacyComboScore = (int)values[ATTRIB_ID_LEGACY_COMBO_SCORE];
- LegacyBonusScoreRatio = (int)values[ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO];
+
+ // Temporarily allow these attributes to not exist so as to not block releases of server-side components while these attributes aren't populated/used yet.
+ LegacyAccuracyScore = (int)values.GetValueOrDefault(ATTRIB_ID_LEGACY_ACCURACY_SCORE);
+ LegacyComboScore = (int)values.GetValueOrDefault(ATTRIB_ID_LEGACY_COMBO_SCORE);
+ LegacyBonusScoreRatio = values.GetValueOrDefault(ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO);
}
}
}
diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs
index 028f8b6839..e6d5949d7c 100644
--- a/osu.Game/Rulesets/Objects/SliderPath.cs
+++ b/osu.Game/Rulesets/Objects/SliderPath.cs
@@ -40,11 +40,13 @@ namespace osu.Game.Rulesets.Objects
private readonly List calculatedPath = new List();
private readonly List cumulativeLength = new List();
- private readonly List segmentEnds = new List();
private readonly Cached pathCache = new Cached();
private double calculatedLength;
+ private readonly List segmentEnds = new List();
+ private double[] segmentEndDistances = Array.Empty();
+
///
/// Creates a new .
///
@@ -202,7 +204,7 @@ namespace osu.Game.Rulesets.Objects
{
ensureValid();
- return segmentEnds.Select(i => cumulativeLength[i] / calculatedLength);
+ return segmentEndDistances.Select(d => d / Distance);
}
private void invalidate()
@@ -251,8 +253,9 @@ namespace osu.Game.Rulesets.Objects
calculatedPath.Add(t);
}
- // Remember the index of the segment end
- segmentEnds.Add(calculatedPath.Count - 1);
+ if (i > 0)
+ // Remember the index of the segment end
+ segmentEnds.Add(calculatedPath.Count - 1);
// Start the new segment at the current vertex
start = i;
@@ -298,6 +301,14 @@ namespace osu.Game.Rulesets.Objects
cumulativeLength.Add(calculatedLength);
}
+ // Store the distances of the segment ends now, because after shortening the indices may be out of range
+ segmentEndDistances = new double[segmentEnds.Count];
+
+ for (int i = 0; i < segmentEnds.Count; i++)
+ {
+ segmentEndDistances[i] = cumulativeLength[segmentEnds[i]];
+ }
+
if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance)
{
// In osu-stable, if the last two control points of a slider are equal, extension is not performed.
@@ -319,10 +330,6 @@ namespace osu.Game.Rulesets.Objects
{
cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
calculatedPath.RemoveAt(pathEndIndex--);
-
- // Shorten the last segment to the expected distance
- if (segmentEnds.Count > 0)
- segmentEnds[^1]--;
}
}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 35d8bb4ab7..1cdca5754d 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -199,6 +199,8 @@ namespace osu.Game.Screens.Edit
if (loadableBeatmap is DummyWorkingBeatmap)
{
+ Logger.Log("Editor was loaded without a valid beatmap; creating a new beatmap.");
+
isNewBeatmap = true;
loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs
index 555c36aac0..22e37b9efb 100644
--- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs
+++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs
@@ -147,13 +147,25 @@ namespace osu.Game.Screens.Edit.Timing
trackedType = null;
else
{
- // If the selected group only has one control point, update the tracking type.
- if (selectedGroup.Value.ControlPoints.Count == 1)
- trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
- // If the selected group has more than one control point, choose the first as the tracking type
- // if we don't already have a singular tracked type.
- else if (trackedType == null)
- trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
+ switch (selectedGroup.Value.ControlPoints.Count)
+ {
+ // If the selected group has no control points, clear the tracked type.
+ // Otherwise the user will be unable to select a group with no control points.
+ case 0:
+ trackedType = null;
+ break;
+
+ // If the selected group only has one control point, update the tracking type.
+ case 1:
+ trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
+ break;
+
+ // If the selected group has more than one control point, choose the first as the tracking type
+ // if we don't already have a singular tracked type.
+ default:
+ trackedType ??= selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
+ break;
+ }
}
if (trackedType != null)
diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
deleted file mode 100644
index 1bf0e5299d..0000000000
--- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Globalization;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Localisation;
-using osu.Game.Graphics.UserInterfaceV2;
-using osu.Game.Overlays.Settings;
-using osu.Game.Utils;
-using osuTK;
-
-namespace osu.Game.Screens.Edit.Timing
-{
- public partial class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue
- where T : struct, IEquatable, IComparable, IConvertible
- {
- private readonly SettingsSlider slider;
-
- public SliderWithTextBoxInput(LocalisableString labelText)
- {
- LabelledTextBox textBox;
-
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
-
- InternalChildren = new Drawable[]
- {
- new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(20),
- Children = new Drawable[]
- {
- textBox = new LabelledTextBox
- {
- Label = labelText,
- },
- slider = new SettingsSlider
- {
- TransferValueOnCommit = true,
- RelativeSizeAxes = Axes.X,
- }
- }
- },
- };
-
- textBox.OnCommit += (t, isNew) =>
- {
- if (!isNew) return;
-
- try
- {
- 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
- {
- // TriggerChange below will restore the previous text value on failure.
- }
-
- // This is run regardless of parsing success as the parsed number may not actually trigger a change
- // due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
- Current.TriggerChange();
- };
-
- Current.BindValueChanged(_ =>
- {
- decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
- textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
- }, true);
- }
-
- ///
- /// A custom step value for each key press which actuates a change on this control.
- ///
- public float KeyboardStep
- {
- get => slider.KeyboardStep;
- set => slider.KeyboardStep = value;
- }
-
- public Bindable Current
- {
- get => slider.Current;
- set => slider.Current = value;
- }
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs
index 45615d4e19..2ce78818a0 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Logging;
using osu.Framework.Timing;
using osu.Game.Screens.Play;
@@ -59,6 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public bool Seek(double position)
{
+ Logger.Log($"{nameof(SpectatorPlayerClock)} seeked to {position}");
CurrentTime = position;
return true;
}
diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs
index 22e6884526..20bf6c3829 100644
--- a/osu.Game/Screens/Play/GameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/GameplayClockContainer.cs
@@ -160,6 +160,21 @@ namespace osu.Game.Screens.Play
Seek(StartTime);
+ // This is a workaround for the fact that DecoupleableInterpolatingFramedClock doesn't seek the source
+ // if the source is not IsRunning. (see https://github.com/ppy/osu-framework/blob/2102638056dfcf85d21b4d85266d53b5dd018767/osu.Framework/Timing/DecoupleableInterpolatingFramedClock.cs#L209-L210)
+ // I hope to remove this once we knock some sense into clocks in general.
+ //
+ // Without this seek, the multiplayer spectator start sequence breaks:
+ // - Individual clients' clocks are never updated to their expected time
+ // - The sync manager thinks they are running behind
+ // - Gameplay doesn't start when it should (until a timeout occurs because nothing is happening for 10+ seconds)
+ //
+ // In addition, we use `CurrentTime` for this seek instead of `StartTime` as the above seek may have applied inherent
+ // offsets which need to be accounted for (ie. FramedBeatmapClock.TotalAppliedOffset).
+ //
+ // See https://github.com/ppy/osu/pull/24451/files/87fee001c786b29db34063ef3350e9a9f024d3ab#diff-28ca02979641e2d98a15fe5d5e806f56acf60ac100258a059fa72503b6cc54e8.
+ (SourceClock as IAdjustableClock)?.Seek(CurrentTime);
+
if (!wasPaused || startClock)
Start();
}
diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
index 58bf4eea4b..4a61c7fd1b 100644
--- a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -24,11 +22,13 @@ namespace osu.Game.Screens.Play.HUD
public BindableLong Team1Score = new BindableLong();
public BindableLong Team2Score = new BindableLong();
- protected MatchScoreCounter Score1Text;
- protected MatchScoreCounter Score2Text;
+ protected MatchScoreCounter Score1Text = null!;
+ protected MatchScoreCounter Score2Text = null!;
- private Drawable score1Bar;
- private Drawable score2Bar;
+ private Drawable score1Bar = null!;
+ private Drawable score2Bar = null!;
+
+ private MatchScoreDiffCounter scoreDiffText = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
@@ -98,6 +98,16 @@ namespace osu.Game.Screens.Play.HUD
},
}
},
+ scoreDiffText = new MatchScoreDiffCounter
+ {
+ Anchor = Anchor.TopCentre,
+ Margin = new MarginPadding
+ {
+ Top = bar_height / 4,
+ Horizontal = 8
+ },
+ Alpha = 0
+ }
};
}
@@ -139,6 +149,10 @@ namespace osu.Game.Screens.Play.HUD
losingBar.ResizeWidthTo(0, 400, Easing.OutQuint);
winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint);
+
+ scoreDiffText.Alpha = diff != 0 ? 1 : 0;
+ scoreDiffText.Current.Value = -diff;
+ scoreDiffText.Origin = Team1Score.Value > Team2Score.Value ? Anchor.TopLeft : Anchor.TopRight;
}
protected override void UpdateAfterChildren()
@@ -150,7 +164,7 @@ namespace osu.Game.Screens.Play.HUD
protected partial class MatchScoreCounter : CommaSeparatedScoreCounter
{
- private OsuSpriteText displayedSpriteText;
+ private OsuSpriteText displayedSpriteText = null!;
public MatchScoreCounter()
{
@@ -174,5 +188,14 @@ namespace osu.Game.Screens.Play.HUD
? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true);
}
+
+ private partial class MatchScoreDiffCounter : CommaSeparatedScoreCounter
+ {
+ protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
+ {
+ s.Spacing = new Vector2(-2);
+ s.Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: bar_height, fixedWidth: true);
+ });
+ }
}
}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 43c61588b1..128f8d5ffd 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -81,6 +81,7 @@ namespace osu.Game.Screens.Play
public Bindable ShowHud { get; } = new BindableBool();
private Bindable configVisibilityMode;
+ private Bindable configLeaderboardVisibility;
private Bindable configSettingsOverlay;
private readonly BindableBool replayLoaded = new BindableBool();
@@ -186,6 +187,7 @@ namespace osu.Game.Screens.Play
ModDisplay.Current.Value = mods;
configVisibilityMode = config.GetBindable(OsuSetting.HUDVisibilityMode);
+ configLeaderboardVisibility = config.GetBindable(OsuSetting.GameplayLeaderboard);
configSettingsOverlay = config.GetBindable(OsuSetting.ReplaySettingsOverlay);
if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce)
@@ -398,6 +400,10 @@ namespace osu.Game.Screens.Play
}
return true;
+
+ case GlobalAction.ToggleInGameLeaderboard:
+ configLeaderboardVisibility.Value = !configLeaderboardVisibility.Value;
+ return true;
}
return false;
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 5f8e061d89..8c5828fc92 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -810,10 +810,13 @@ namespace osu.Game.Screens.Play
if (!canShowResults && !forceImport)
return Task.FromResult(null);
+ // Clone score before beginning any async processing.
+ // - Must be run synchronously as the score may potentially be mutated in the background.
+ // - Must be cloned for the same reason.
+ Score scoreCopy = Score.DeepClone();
+
return prepareScoreForDisplayTask = Task.Run(async () =>
{
- var scoreCopy = Score.DeepClone();
-
try
{
await PrepareScoreForResultsAsync(scoreCopy).ConfigureAwait(false);
diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
index 305a615102..5db08810ca 100644
--- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
+++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
@@ -124,7 +124,12 @@ namespace osu.Game.Tests.Visual.Spectator
if (frames.Count == 0)
return;
- var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray());
+ var bundle = new FrameDataBundle(new ScoreInfo
+ {
+ Combo = currentFrameIndex,
+ TotalScore = (long)(currentFrameIndex * 123478 * RNG.NextDouble(0.99, 1.01)),
+ Accuracy = RNG.NextDouble(0.98, 1),
+ }, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray());
((ISpectatorClient)this).UserSentFrames(userId, bundle);
frames.Clear();
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 1ac0f9de73..a5a9387e36 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 9926d52413..d93dfaf67c 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -23,6 +23,6 @@
iossimulator-x64
-
+