diff --git a/osu.Android.props b/osu.Android.props
index d64855e5c1..8ebfde8047 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -10,7 +10,7 @@
true
-
+
+
+
\ No newline at end of file
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 5a1373e040..a33e845f5b 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -85,7 +85,7 @@ namespace osu.Desktop
}
}
- using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true }))
+ using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = !tournamentClient }))
{
if (!host.IsPrimaryInstance)
{
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
index bf7c0bfeca..52b34959b9 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
index 4a1545a423..f5a49210ea 100644
--- a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs
index 3011a93755..f5117b61af 100644
--- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs
@@ -6,7 +6,6 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
-using System.Collections.Generic;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -24,21 +23,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
Assert.False(testBeatmap.HitObjects.OfType().Any());
}
- [Test]
- public void TestCorrectNoteValues()
- {
- var testBeatmap = createRawBeatmap();
- var noteValues = new List(testBeatmap.HitObjects.OfType().Count());
-
- foreach (HoldNote h in testBeatmap.HitObjects.OfType())
- {
- noteValues.Add(ManiaModHoldOff.GetNoteDurationInBeatLength(h, testBeatmap));
- }
-
- noteValues.Sort();
- Assert.AreEqual(noteValues, new List { 0.125, 0.250, 0.500, 1.000, 2.000 });
- }
-
[Test]
public void TestCorrectObjectCount()
{
@@ -47,25 +31,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
var rawBeatmap = createRawBeatmap();
var testBeatmap = createModdedBeatmap();
- // Calculate expected number of objects
- int expectedObjectCount = 0;
-
- foreach (ManiaHitObject h in rawBeatmap.HitObjects)
- {
- // Both notes and hold notes account for at least one object
- expectedObjectCount++;
-
- if (h.GetType() == typeof(HoldNote))
- {
- double noteValue = ManiaModHoldOff.GetNoteDurationInBeatLength((HoldNote)h, rawBeatmap);
-
- if (noteValue >= ManiaModHoldOff.END_NOTE_ALLOW_THRESHOLD)
- {
- // Should generate an end note if it's longer than the minimum note value
- expectedObjectCount++;
- }
- }
- }
+ // Both notes and hold notes account for at least one object
+ int expectedObjectCount = rawBeatmap.HitObjects.Count;
Assert.That(testBeatmap.HitObjects.Count == expectedObjectCount);
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
index 06c825e37d..a24fcaad8d 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
- private const double release_threshold = 24;
+ private const double release_threshold = 30;
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1;
@@ -50,10 +50,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
for (int i = 0; i < endTimes.Length; ++i)
{
// The current note is overlapped if a previous note or end is overlapping the current note body
- isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && Precision.DefinitelyBigger(endTime, endTimes[i], 1);
+ isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) &&
+ Precision.DefinitelyBigger(endTime, endTimes[i], 1) &&
+ Precision.DefinitelyBigger(startTime, startTimes[i], 1);
// We give a slight bonus to everything if something is held meanwhile
- if (Precision.DefinitelyBigger(endTimes[i], endTime, 1))
+ if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) &&
+ Precision.DefinitelyBigger(startTime, startTimes[i], 1))
holdFactor = 1.25;
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
@@ -70,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
// 0.0 +--------+-+---------------> 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/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.Osu.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
index 45d27dda70..ed4725dd94 100644
--- a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
index 0c064ecfa6..9338d5453d 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
@@ -9,6 +9,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
@@ -70,12 +71,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
base.Content.Children = new Drawable[]
{
editorClock = new EditorClock(editorBeatmap),
- snapProvider,
+ new PopoverContainer { Child = snapProvider },
Content
};
}
- protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
+ protected override Container Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
[SetUp]
public void Setup() => Schedule(() =>
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs
new file mode 100644
index 0000000000..d7dd30d608
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs
@@ -0,0 +1,95 @@
+// 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.Graphics.UserInterface;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Edit;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Screens.Edit.Components.RadioButtons;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ public partial class TestScenePreciseRotation : TestSceneOsuEditor
+ {
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset);
+
+ [Test]
+ public void TestHotkeyHandling()
+ {
+ AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType().First()));
+ AddStep("press rotate hotkey", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.R);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddUntilStep("no popover present", () => this.ChildrenOfType().Count(), () => Is.Zero);
+
+ AddStep("select first three objects", () =>
+ {
+ EditorBeatmap.SelectedHitObjects.Clear();
+ EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects.Take(3));
+ });
+ AddStep("press rotate hotkey", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.R);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddUntilStep("popover present", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1));
+ AddStep("press rotate hotkey", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.R);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddUntilStep("no popover present", () => this.ChildrenOfType().Count(), () => Is.Zero);
+ }
+
+ [Test]
+ public void TestRotateCorrectness()
+ {
+ AddStep("replace objects", () =>
+ {
+ EditorBeatmap.Clear();
+ EditorBeatmap.AddRange(new HitObject[]
+ {
+ new HitCircle { Position = new Vector2(100) },
+ new HitCircle { Position = new Vector2(200) },
+ });
+ });
+ AddStep("select both circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ AddStep("press rotate hotkey", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.R);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddUntilStep("popover present", getPopover, () => Is.Not.Null);
+
+ AddStep("rotate by 180deg", () => getPopover().ChildrenOfType().Single().Current.Value = "180");
+ AddAssert("first object rotated 180deg around playfield centre",
+ () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position,
+ () => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(100)));
+ AddAssert("second object rotated 180deg around playfield centre",
+ () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position,
+ () => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200)));
+
+ AddStep("change rotation origin", () => getPopover().ChildrenOfType().ElementAt(1).TriggerClick());
+ AddAssert("first object rotated 90deg around selection centre",
+ () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200)));
+ AddAssert("second object rotated 90deg around selection centre",
+ () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, () => Is.EqualTo(new Vector2(100, 100)));
+
+ PreciseRotationPopover? getPopover() => this.ChildrenOfType().SingleOrDefault();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 0b80750a02..cff2171cbd 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -85,6 +85,11 @@ namespace osu.Game.Rulesets.Osu.Edit
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
+
+ RightToolbox.Add(new TransformToolboxGroup
+ {
+ RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler
+ });
}
protected override ComposeBlueprintContainer CreateBlueprintContainer()
diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs
new file mode 100644
index 0000000000..f09d6b78e6
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs
@@ -0,0 +1,107 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Screens.Edit.Components.RadioButtons;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public partial class PreciseRotationPopover : OsuPopover
+ {
+ private readonly SelectionRotationHandler rotationHandler;
+
+ private readonly Bindable rotationInfo = new Bindable(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre));
+
+ private SliderWithTextBoxInput angleInput = null!;
+ private EditorRadioButtonCollection rotationOrigin = null!;
+
+ public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
+ {
+ this.rotationHandler = rotationHandler;
+
+ AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Child = new FillFlowContainer
+ {
+ Width = 220,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(20),
+ Children = new Drawable[]
+ {
+ angleInput = new SliderWithTextBoxInput("Angle (degrees):")
+ {
+ Current = new BindableNumber
+ {
+ MinValue = -360,
+ MaxValue = 360,
+ Precision = 1
+ },
+ Instantaneous = true
+ },
+ rotationOrigin = new EditorRadioButtonCollection
+ {
+ RelativeSizeAxes = Axes.X,
+ Items = new[]
+ {
+ new RadioButton("Playfield centre",
+ () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
+ () => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
+ new RadioButton("Selection centre",
+ () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre },
+ () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
+ }
+ }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ ScheduleAfterChildren(() => angleInput.TakeFocus());
+ angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
+ rotationOrigin.Items.First().Select();
+
+ rotationInfo.BindValueChanged(rotation =>
+ {
+ rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
+ });
+ }
+
+ protected override void PopIn()
+ {
+ base.PopIn();
+ rotationHandler.Begin();
+ }
+
+ protected override void PopOut()
+ {
+ base.PopOut();
+
+ if (IsLoaded)
+ rotationHandler.Commit();
+ }
+ }
+
+ public enum RotationOrigin
+ {
+ PlayfieldCentre,
+ SelectionCentre
+ }
+
+ public record PreciseRotationInfo(float Degrees, RotationOrigin Origin);
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs
new file mode 100644
index 0000000000..3da9f5b69b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs
@@ -0,0 +1,80 @@
+// 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.Sprites;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Input.Bindings;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Screens.Edit.Components;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler
+ {
+ private readonly Bindable canRotate = new BindableBool();
+
+ private EditorToolButton rotateButton = null!;
+
+ public SelectionRotationHandler RotationHandler { get; init; } = null!;
+
+ public TransformToolboxGroup()
+ : base("transform")
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Child = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(5),
+ Children = new Drawable[]
+ {
+ rotateButton = new EditorToolButton("Rotate",
+ () => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
+ () => new PreciseRotationPopover(RotationHandler)),
+ // TODO: scale
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // bindings to `Enabled` on the buttons are decoupled on purpose
+ // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
+ canRotate.BindTo(RotationHandler.CanRotate);
+ canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (e.Repeat) return false;
+
+ switch (e.Action)
+ {
+ case GlobalAction.EditorToggleRotateControl:
+ {
+ rotateButton.TriggerClick();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
index 9d64c354e2..d818c8baee 100644
--- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
@@ -257,7 +257,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
texture.Bind();
for (int i = 0; i < points.Count; i++)
- drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
+ drawPointQuad(renderer, points[i], textureRect, i + firstVisiblePointIndex);
UnbindTextureShader(renderer);
renderer.PopLocalMatrix();
@@ -325,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1);
- private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index)
+ private void drawPointQuad(IRenderer renderer, SmokePoint point, RectangleF textureRect, int index)
{
Debug.Assert(quadBatch != null);
@@ -347,25 +347,25 @@ namespace osu.Game.Rulesets.Osu.Skinning
var localBotLeft = point.Position + ortho - dir;
var localBotRight = point.Position + ortho + dir;
- quadBatch.Add(new TexturedVertex2D
+ quadBatch.Add(new TexturedVertex2D(renderer)
{
Position = localTopLeft,
TexturePosition = textureRect.TopLeft,
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
});
- quadBatch.Add(new TexturedVertex2D
+ quadBatch.Add(new TexturedVertex2D(renderer)
{
Position = localTopRight,
TexturePosition = textureRect.TopRight,
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
});
- quadBatch.Add(new TexturedVertex2D
+ quadBatch.Add(new TexturedVertex2D(renderer)
{
Position = localBotRight,
TexturePosition = textureRect.BottomRight,
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
});
- quadBatch.Add(new TexturedVertex2D
+ quadBatch.Add(new TexturedVertex2D(renderer)
{
Position = localBotLeft,
TexturePosition = textureRect.BottomLeft,
diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
index 452b9683ec..cc88d3080a 100644
--- a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
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.Android/AndroidManifest.xml b/osu.Game.Tests.Android/AndroidManifest.xml
index f25b2e5328..6f91fb928c 100644
--- a/osu.Game.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs
index b237556d11..0144c0bf97 100644
--- a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs
+++ b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs
@@ -1,19 +1,23 @@
// 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.Collections.Generic;
using System.IO;
+using System.IO.Compression;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Platform;
using osu.Framework.Testing;
+using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Database
{
[TestFixture]
- public class LegacyBeatmapImporterTest
+ public class LegacyBeatmapImporterTest : RealmTest
{
private readonly TestLegacyBeatmapImporter importer = new TestLegacyBeatmapImporter();
@@ -60,6 +64,33 @@ namespace osu.Game.Tests.Database
}
}
+ [Test]
+ public void TestStableDateAddedApplied()
+ {
+ RunTestWithRealmAsync(async (realm, storage) =>
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
+ using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder"))
+ {
+ var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host);
+ var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH);
+
+ ZipFile.ExtractToDirectory(TestResources.GetQuickTestBeatmapForImport(), songsStorage.GetFullPath("renatus"));
+
+ string[] beatmaps = Directory.GetFiles(songsStorage.GetFullPath("renatus"), "*.osu", SearchOption.TopDirectoryOnly);
+
+ File.SetLastWriteTimeUtc(beatmaps[beatmaps.Length / 2], new DateTime(2000, 1, 1, 12, 0, 0));
+
+ await new LegacyBeatmapImporter(new BeatmapImporter(storage, realm)).ImportFromStableAsync(stableStorage);
+
+ var importedSet = realm.Realm.All().Single();
+
+ Assert.NotNull(importedSet);
+ Assert.AreEqual(new DateTimeOffset(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Utc)), importedSet.DateAdded);
+ }
+ });
+ }
+
private class TestLegacyBeatmapImporter : LegacyBeatmapImporter
{
public TestLegacyBeatmapImporter()
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index 6399507aa0..e30caac95e 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
@@ -29,7 +30,7 @@ namespace osu.Game.Tests.Editing
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
- protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
+ protected override Container Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
public TestSceneHitObjectComposerDistanceSnapping()
{
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/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
index a38c481003..ed3bffe5c2 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
@@ -8,7 +8,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
}
- public partial class EditorBeatmapContainer : Container
+ public partial class EditorBeatmapContainer : PopoverContainer
{
private readonly IWorkingBeatmap working;
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/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs
index c840b4fa94..14719da1bc 100644
--- a/osu.Game/Beatmaps/BeatmapImporter.cs
+++ b/osu.Game/Beatmaps/BeatmapImporter.cs
@@ -152,6 +152,8 @@ namespace osu.Game.Beatmaps
if (archive != null)
beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm));
+ beatmapSet.DateAdded = getDateAdded(archive);
+
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
{
b.BeatmapSet = beatmapSet;
@@ -305,11 +307,36 @@ namespace osu.Game.Beatmaps
return new BeatmapSetInfo
{
OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID ?? -1,
- // Metadata = beatmap.Metadata,
- DateAdded = DateTimeOffset.UtcNow
};
}
+ ///
+ /// Determine the date a given beatmapset has been added to the game.
+ /// For legacy imports, we can use the oldest file write time for any `.osu` file in the directory.
+ /// For any other import types, use "now".
+ ///
+ private DateTimeOffset getDateAdded(ArchiveReader? reader)
+ {
+ DateTimeOffset dateAdded = DateTimeOffset.UtcNow;
+
+ if (reader is LegacyDirectoryArchiveReader legacyReader)
+ {
+ var beatmaps = reader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
+
+ dateAdded = File.GetLastWriteTimeUtc(legacyReader.GetFullPath(beatmaps.First()));
+
+ foreach (string beatmapName in beatmaps)
+ {
+ var currentDateAdded = File.GetLastWriteTimeUtc(legacyReader.GetFullPath(beatmapName));
+
+ if (currentDateAdded < dateAdded)
+ dateAdded = currentDateAdded;
+ }
+ }
+
+ return dateAdded;
+ }
+
///
/// Create all required s for the provided archive.
///
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/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/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
index 454be02d0b..8b9d35e343 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
@@ -35,6 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public string Text
{
+ get => Component.Text;
set => Component.Text = value;
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs
new file mode 100644
index 0000000000..37ea2a3f96
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs
@@ -0,0 +1,145 @@
+// 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);
+ }
+
+ public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox);
+
+ 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/IO/Archives/LegacyDirectoryArchiveReader.cs b/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs
index dfae58aed7..1503705022 100644
--- a/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs
+++ b/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs
@@ -21,7 +21,9 @@ namespace osu.Game.IO.Archives
this.path = Path.GetFullPath(path);
}
- public override Stream GetStream(string name) => File.OpenRead(Path.Combine(path, name));
+ public override Stream GetStream(string name) => File.OpenRead(GetFullPath(name));
+
+ public string GetFullPath(string filename) => Path.Combine(path, filename);
public override void Dispose()
{
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 01c454e3f9..9a0a2d5c15 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -105,6 +105,7 @@ namespace osu.Game.Input.Bindings
// See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38.
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
+ new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
};
public IEnumerable InGameKeyBindings => new[]
@@ -116,9 +117,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 +206,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 +233,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 +260,6 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.PauseGameplay))]
PauseGameplay,
- // Editor
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSetupMode))]
EditorSetupMode,
@@ -285,7 +284,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 +376,11 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleReplaySettings))]
ToggleReplaySettings,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameLeaderboard))]
+ ToggleInGameLeaderboard,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
+ EditorToggleRotateControl,
}
}
diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
index f93d86225c..8356c480dd 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"
///
@@ -339,6 +344,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay");
+ ///
+ /// "Toggle rotate control"
+ ///
+ public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control");
+
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
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/Screens/Edit/Components/EditorToolButton.cs b/osu.Game/Screens/Edit/Components/EditorToolButton.cs
new file mode 100644
index 0000000000..6550362687
--- /dev/null
+++ b/osu.Game/Screens/Edit/Components/EditorToolButton.cs
@@ -0,0 +1,107 @@
+// 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.Extensions;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Localisation;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit.Components
+{
+ public partial class EditorToolButton : OsuButton, IHasPopover
+ {
+ public BindableBool Selected { get; } = new BindableBool();
+
+ private readonly Func createIcon;
+ private readonly Func createPopover;
+
+ private Color4 defaultBackgroundColour;
+ private Color4 defaultIconColour;
+ private Color4 selectedBackgroundColour;
+ private Color4 selectedIconColour;
+
+ private Drawable icon = null!;
+
+ public EditorToolButton(LocalisableString text, Func createIcon, Func createPopover)
+ {
+ Text = text;
+ this.createIcon = createIcon;
+ this.createPopover = createPopover;
+
+ RelativeSizeAxes = Axes.X;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ defaultBackgroundColour = colourProvider.Background3;
+ selectedBackgroundColour = colourProvider.Background1;
+
+ defaultIconColour = defaultBackgroundColour.Darken(0.5f);
+ selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
+
+ Add(icon = createIcon().With(b =>
+ {
+ b.Blending = BlendingParameters.Additive;
+ b.Anchor = Anchor.CentreLeft;
+ b.Origin = Anchor.CentreLeft;
+ b.Size = new Vector2(20);
+ b.X = 10;
+ }));
+
+ Action = Selected.Toggle;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Selected.BindValueChanged(_ => updateSelectionState(), true);
+ }
+
+ private void updateSelectionState()
+ {
+ if (!IsLoaded)
+ return;
+
+ BackgroundColour = Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
+ icon.Colour = Selected.Value ? selectedIconColour : defaultIconColour;
+
+ if (Selected.Value)
+ this.ShowPopover();
+ else
+ this.HidePopover();
+ }
+
+ protected override SpriteText CreateText() => new OsuSpriteText
+ {
+ Depth = -1,
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ X = 40f
+ };
+
+ public Popover? GetPopover() => Enabled.Value
+ ? createPopover()?.With(p =>
+ {
+ p.State.BindValueChanged(state =>
+ {
+ if (state.NewValue == Visibility.Hidden)
+ Selected.Value = false;
+ });
+ })
+ : null;
+ }
+}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 1de6c8364c..110beb0fa6 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
public Container> SelectionBlueprints { get; private set; }
- protected SelectionHandler SelectionHandler { get; private set; }
+ public SelectionHandler SelectionHandler { get; private set; }
private readonly Dictionary> blueprintMap = new Dictionary>();
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index 158b4066bc..3c859c65ff 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved(CanBeNull = true)]
protected IEditorChangeHandler ChangeHandler { get; private set; }
- protected SelectionRotationHandler RotationHandler { get; private set; }
+ public SelectionRotationHandler RotationHandler { get; private set; }
protected SelectionHandler()
{
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/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 a5a9387e36..7d4a721c91 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 d93dfaf67c..fd1ba0f6d1 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -23,6 +23,6 @@
iossimulator-x64
-
+