diff --git a/osu.Android.props b/osu.Android.props
index 1ac85532b8..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/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
index 5d950eef55..d1ac42f22b 100644
--- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
+++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
@@ -75,7 +75,7 @@ namespace osu.Desktop.LegacyIpc
case LegacyIpcDifficultyCalculationRequest req:
try
{
- WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile);
+ WorkingBeatmap beatmap = new FlatWorkingBeatmap(req.BeatmapFile);
var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance();
Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray();
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.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
index 86499a7c6e..fee3ba3e39 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
@@ -117,18 +117,16 @@ namespace osu.Game.Rulesets.Mania.Tests
private void createBarLine(bool major)
{
- foreach (var stage in stages)
+ var obj = new BarLine
{
- var obj = new BarLine
- {
- StartTime = Time.Current + 2000,
- Major = major,
- };
+ StartTime = Time.Current + 2000,
+ Major = major,
+ };
- obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ foreach (var stage in stages)
stage.Add(obj);
- }
}
private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action)
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/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.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.Tests/Editor/TestSceneSliderReversal.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs
new file mode 100644
index 0000000000..9c5eb83e3c
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs
@@ -0,0 +1,110 @@
+// 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.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ public partial class TestSceneSliderReversal : TestSceneOsuEditor
+ {
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
+
+ private readonly PathControlPoint[][] paths =
+ {
+ createPathSegment(
+ PathType.PerfectCurve,
+ new Vector2(200, -50),
+ new Vector2(250, 0)
+ ),
+ createPathSegment(
+ PathType.Linear,
+ new Vector2(100, 0),
+ new Vector2(100, 100)
+ )
+ };
+
+ private static PathControlPoint[] createPathSegment(PathType type, params Vector2[] positions)
+ {
+ return positions.Select(p => new PathControlPoint
+ {
+ Position = p
+ }).Prepend(new PathControlPoint
+ {
+ Type = type
+ }).ToArray();
+ }
+
+ private Slider selectedSlider => (Slider)EditorBeatmap.SelectedHitObjects[0];
+
+ [TestCase(0, 250)]
+ [TestCase(0, 200)]
+ [TestCase(1, 120)]
+ [TestCase(1, 80)]
+ public void TestSliderReversal(int pathIndex, double length)
+ {
+ var controlPoints = paths[pathIndex];
+
+ Vector2 oldStartPos = default;
+ Vector2 oldEndPos = default;
+ double oldDistance = default;
+ var oldControlPointTypes = controlPoints.Select(p => p.Type);
+
+ AddStep("Add slider", () =>
+ {
+ var slider = new Slider
+ {
+ Position = new Vector2(OsuPlayfield.BASE_SIZE.X / 2, OsuPlayfield.BASE_SIZE.Y / 2),
+ Path = new SliderPath(controlPoints)
+ {
+ ExpectedDistance = { Value = length }
+ }
+ };
+
+ EditorBeatmap.Add(slider);
+
+ oldStartPos = slider.Position;
+ oldEndPos = slider.EndPosition;
+ oldDistance = slider.Path.Distance;
+ });
+
+ AddStep("Select slider", () =>
+ {
+ var slider = (Slider)EditorBeatmap.HitObjects[0];
+ EditorBeatmap.SelectedHitObjects.Add(slider);
+ });
+
+ AddStep("Reverse slider", () =>
+ {
+ InputManager.PressKey(Key.LControl);
+ InputManager.Key(Key.G);
+ InputManager.ReleaseKey(Key.LControl);
+ });
+
+ AddAssert("Slider has correct length", () =>
+ Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
+
+ AddAssert("Slider has correct start position", () =>
+ Vector2.Distance(selectedSlider.Position, oldEndPos) < 1);
+
+ AddAssert("Slider has correct end position", () =>
+ Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1);
+
+ AddAssert("Control points have correct types", () =>
+ {
+ var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray();
+
+ return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes);
+ });
+ }
+ }
+}
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.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index a29faac5a0..0774d34488 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
if (time - part.Time >= 1)
continue;
- vertexBatch.Add(new TexturedTrailVertex
+ vertexBatch.Add(new TexturedTrailVertex(renderer)
{
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomLeft,
@@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time
});
- vertexBatch.Add(new TexturedTrailVertex
+ vertexBatch.Add(new TexturedTrailVertex(renderer)
{
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomRight,
@@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time
});
- vertexBatch.Add(new TexturedTrailVertex
+ vertexBatch.Add(new TexturedTrailVertex(renderer)
{
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopRight,
@@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time
});
- vertexBatch.Add(new TexturedTrailVertex
+ vertexBatch.Add(new TexturedTrailVertex(renderer)
{
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopLeft,
@@ -362,12 +362,22 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
[VertexMember(1, VertexAttribPointerType.Float)]
public float Time;
+ [VertexMember(1, VertexAttribPointerType.Int)]
+ private readonly int maskingIndex;
+
+ public TexturedTrailVertex(IRenderer renderer)
+ {
+ this = default;
+ maskingIndex = renderer.CurrentMaskingIndex;
+ }
+
public bool Equals(TexturedTrailVertex other)
{
return Position.Equals(other.Position)
&& TexturePosition.Equals(other.TexturePosition)
&& Colour.Equals(other.Colour)
- && Time.Equals(other.Time);
+ && Time.Equals(other.Time)
+ && maskingIndex == other.maskingIndex;
}
}
}
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/BackgroundBeatmapProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs
similarity index 60%
rename from osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs
rename to osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs
index c876316be4..da46392e4b 100644
--- a/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs
+++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs
@@ -8,6 +8,9 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Scoring;
+using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
@@ -15,7 +18,7 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Database
{
[HeadlessTest]
- public partial class BackgroundBeatmapProcessorTests : OsuTestScene, ILocalUserPlayInfo
+ public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo
{
public IBindable IsPlaying => isPlaying;
@@ -59,7 +62,7 @@ namespace osu.Game.Tests.Database
AddStep("Run background processor", () =>
{
- Add(new TestBackgroundBeatmapProcessor());
+ Add(new TestBackgroundDataStoreProcessor());
});
AddUntilStep("wait for difficulties repopulated", () =>
@@ -98,7 +101,7 @@ namespace osu.Game.Tests.Database
AddStep("Run background processor", () =>
{
- Add(new TestBackgroundBeatmapProcessor());
+ Add(new TestBackgroundDataStoreProcessor());
});
AddWaitStep("wait some", 500);
@@ -124,7 +127,58 @@ namespace osu.Game.Tests.Database
});
}
- public partial class TestBackgroundBeatmapProcessor : BackgroundBeatmapProcessor
+ [Test]
+ public void TestScoreUpgradeSuccess()
+ {
+ ScoreInfo scoreInfo = null!;
+
+ AddStep("Add score which requires upgrade (and has beatmap)", () =>
+ {
+ Realm.Write(r =>
+ {
+ r.Add(scoreInfo = new ScoreInfo(ruleset: r.All().First(), beatmap: r.All().First())
+ {
+ TotalScoreVersion = 30000002,
+ LegacyTotalScore = 123456,
+ IsLegacyScore = true,
+ });
+ });
+ });
+
+ AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
+
+ AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION));
+ AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
+ }
+
+ [Test]
+ public void TestScoreUpgradeFailed()
+ {
+ ScoreInfo scoreInfo = null!;
+
+ AddStep("Add score which requires upgrade (but has no beatmap)", () =>
+ {
+ Realm.Write(r =>
+ {
+ r.Add(scoreInfo = new ScoreInfo(ruleset: r.All().First(), beatmap: new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo(),
+ Ruleset = r.All().First(),
+ })
+ {
+ TotalScoreVersion = 30000002,
+ IsLegacyScore = true,
+ });
+ });
+ });
+
+ AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
+
+ AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
+ AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002));
+ }
+
+ public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor
{
protected override int TimeToSleepDuringGameplay => 10;
}
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/Resources/Shaders/sh_TestVertex.vs b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs
index 505554bb33..80ed686ba5 100644
--- a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs
+++ b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs
@@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange;
void main(void)
{
// Transform from screen space to masking space.
- highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
+ highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0);
v_MaskingPosition = maskingPos.xy / maskingPos.z;
v_Colour = m_Colour;
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/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
index dfa9fdf03b..635d9f9604 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
@@ -185,6 +185,37 @@ 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(), () => Is.EqualTo(distances.Select(d => d / 300)));
+ AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));
+
+ AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 400);
+ AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 400)));
+ AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));
+
+ AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
+ AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 150)));
+ // see remarks in `GetSegmentEnds()` xmldoc (`SliderPath.PositionAt()` clamps progress to [0,1]).
+ AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(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/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs
index b12f3e7946..bb327e5962 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Menus
foreach (var fountain in Children.OfType())
{
if (RNG.NextSingle() > 0.8f)
- fountain.Shoot();
+ fountain.Shoot(RNG.Next(-1, 2));
}
}, 150);
}
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..e0444b6126 100644
--- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs
+++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs
@@ -2,25 +2,35 @@
// See the LICENCE file in the repository root for full licence text.
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;
using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Tests.Components
{
[TestFixture]
- public partial class TestSceneSongBar : OsuTestScene
+ public partial class TestSceneSongBar : TournamentTestScene
{
- [Cached]
- private readonly LadderInfo ladder = new LadderInfo();
+ private SongBar songBar = null!;
+ private TournamentBeatmap ladderBeatmap = null!;
- [Test]
- public void TestSongBar()
+ [SetUpSteps]
+ public override void SetUpSteps()
{
- SongBar songBar = null!;
+ base.SetUpSteps();
+
+ AddStep("setup picks bans", () =>
+ {
+ ladderBeatmap = CreateSampleBeatmap();
+ Ladder.CurrentMatch.Value!.PicksBans.Add(new BeatmapChoice
+ {
+ BeatmapID = ladderBeatmap.OnlineID,
+ Team = TeamColour.Red,
+ Type = ChoiceType.Pick,
+ });
+ });
AddStep("create bar", () => Child = songBar = new SongBar
{
@@ -29,16 +39,22 @@ 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);
+
beatmap.CircleSize = 3.4f;
beatmap.ApproachRate = 6.8f;
beatmap.OverallDifficulty = 5.5f;
beatmap.StarRating = 4.56f;
beatmap.Length = 123456;
beatmap.BPM = 133;
+ beatmap.OnlineID = ladderBeatmap.OnlineID;
songBar.Beatmap = new TournamentBeatmap(beatmap);
});
diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs
index fa0cbda16b..cde826628e 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)
{
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/Models/TournamentTeam.cs b/osu.Game.Tournament/Models/TournamentTeam.cs
index 5df8c2a620..ae0ac77936 100644
--- a/osu.Game.Tournament/Models/TournamentTeam.cs
+++ b/osu.Game.Tournament/Models/TournamentTeam.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Tournament.Models
public Bindable LastYearPlacing = new BindableInt
{
- MinValue = 1,
+ MinValue = 0,
MaxValue = 256
};
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index 241692d515..250d5acaae 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -10,7 +10,9 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Models;
@@ -128,7 +130,7 @@ namespace osu.Game.Tournament.Screens.Editors
Width = 0.2f,
Current = Model.Seed
},
- new SettingsSlider
+ new SettingsSlider
{
LabelText = "Last Year Placement",
Width = 0.33f,
@@ -175,6 +177,11 @@ namespace osu.Game.Tournament.Screens.Editors
};
}
+ private partial class LastYearPlacementSlider : RoundedSliderBar
+ {
+ public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText;
+ }
+
public partial class PlayerEditor : CompositeDrawable
{
private readonly TournamentTeam team;
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.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
index 120a76c127..899d462e4e 100644
--- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
@@ -274,7 +274,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
new TeamDisplay(team) { Margin = new MarginPadding { Bottom = 30 } },
new RowDisplay("Average Rank:", $"#{team.AverageRank:#,0}"),
new RowDisplay("Seed:", team.Seed.Value),
- new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "0"),
+ new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "N/A"),
new Container { Margin = new MarginPadding { Bottom = 30 } },
}
},
diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundDataStoreProcessor.cs
similarity index 93%
rename from osu.Game/BackgroundBeatmapProcessor.cs
rename to osu.Game/BackgroundDataStoreProcessor.cs
index b553fee503..f29b100ee8 100644
--- a/osu.Game/BackgroundBeatmapProcessor.cs
+++ b/osu.Game/BackgroundDataStoreProcessor.cs
@@ -24,7 +24,10 @@ using osu.Game.Screens.Play;
namespace osu.Game
{
- public partial class BackgroundBeatmapProcessor : Component
+ ///
+ /// Performs background updating of data stores at startup.
+ ///
+ public partial class BackgroundDataStoreProcessor : Component
{
[Resolved]
private RulesetStore rulesetStore { get; set; } = null!;
@@ -61,7 +64,8 @@ namespace osu.Game
Task.Factory.StartNew(() =>
{
- Logger.Log("Beginning background beatmap processing..");
+ Logger.Log("Beginning background data store processing..");
+
checkForOutdatedStarRatings();
processBeatmapSetsWithMissingMetrics();
processScoresWithMissingStatistics();
@@ -74,7 +78,7 @@ namespace osu.Game
return;
}
- Logger.Log("Finished background beatmap processing!");
+ Logger.Log("Finished background data store processing!");
});
}
@@ -182,7 +186,7 @@ namespace osu.Game
realmAccess.Run(r =>
{
- foreach (var score in r.All())
+ foreach (var score in r.All().Where(s => !s.BackgroundReprocessingFailed))
{
if (score.BeatmapInfo != null
&& score.Statistics.Sum(kvp => kvp.Value) > 0
@@ -221,6 +225,7 @@ namespace osu.Game
catch (Exception e)
{
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
+ realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true);
}
}
}
@@ -230,7 +235,7 @@ namespace osu.Game
Logger.Log("Querying for scores that need total score conversion...");
HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All()
- .Where(s => s.BeatmapInfo != null && s.TotalScoreVersion == 30000002)
+ .Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null && s.TotalScoreVersion == 30000002)
.AsEnumerable().Select(s => s.ID)));
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
@@ -279,6 +284,7 @@ namespace osu.Game
catch (Exception e)
{
Logger.Log($"Failed to convert total score for {id}: {e}");
+ realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true);
++failedCount;
}
}
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/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/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs
index 4f0f11d053..c007f5dcdc 100644
--- a/osu.Game/Beatmaps/Formats/Decoder.cs
+++ b/osu.Game/Beatmaps/Formats/Decoder.cs
@@ -1,13 +1,10 @@
// 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 System.Collections.Generic;
using System.IO;
using System.Linq;
-using JetBrains.Annotations;
using osu.Game.IO;
using osu.Game.Rulesets;
@@ -45,7 +42,7 @@ namespace osu.Game.Beatmaps.Formats
/// Register dependencies for use with static decoder classes.
///
/// A store containing all available rulesets (used by ).
- public static void RegisterDependencies([NotNull] RulesetStore rulesets)
+ public static void RegisterDependencies(RulesetStore rulesets)
{
LegacyBeatmapDecoder.RulesetStore = rulesets ?? throw new ArgumentNullException(nameof(rulesets));
}
@@ -63,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats
throw new IOException(@"Unknown decoder type");
// start off with the first line of the file
- string line = stream.PeekLine()?.Trim();
+ string? line = stream.PeekLine()?.Trim();
while (line != null && line.Length == 0)
{
diff --git a/osu.Game/Beatmaps/Formats/IHasComboColours.cs b/osu.Game/Beatmaps/Formats/IHasComboColours.cs
index 1d9cc0be65..1608adee7d 100644
--- a/osu.Game/Beatmaps/Formats/IHasComboColours.cs
+++ b/osu.Game/Beatmaps/Formats/IHasComboColours.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.Collections.Generic;
using osuTK.Graphics;
@@ -13,7 +11,7 @@ namespace osu.Game.Beatmaps.Formats
///
/// Retrieves the list of combo colours for presentation only.
///
- IReadOnlyList ComboColours { get; }
+ IReadOnlyList? ComboColours { get; }
///
/// The list of custom combo colours.
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 65a01befb4..5da51f6c8e 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.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
-
#pragma warning disable 618
using System;
@@ -36,11 +34,11 @@ namespace osu.Game.Beatmaps.Formats
///
private const double control_point_leniency = 1;
- internal static RulesetStore RulesetStore;
+ internal static RulesetStore? RulesetStore;
- private Beatmap beatmap;
+ private Beatmap beatmap = null!;
- private ConvertHitObjectParser parser;
+ private ConvertHitObjectParser? parser;
private LegacySampleBank defaultSampleBank;
private int defaultSampleVolume = 100;
@@ -222,7 +220,7 @@ namespace osu.Game.Beatmaps.Formats
case @"Mode":
int rulesetID = Parsing.ParseInt(pair.Value);
- beatmap.BeatmapInfo.Ruleset = RulesetStore.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally.");
+ beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally.");
switch (rulesetID)
{
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 041b00c7e1..fc9de13c89 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -1,15 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
-using JetBrains.Annotations;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
@@ -34,8 +31,7 @@ namespace osu.Game.Beatmaps.Formats
private readonly IBeatmap beatmap;
- [CanBeNull]
- private readonly ISkin skin;
+ private readonly ISkin? skin;
private readonly int onlineRulesetID;
@@ -44,7 +40,7 @@ namespace osu.Game.Beatmaps.Formats
///
/// The beatmap to encode.
/// The beatmap's skin, used for encoding combo colours.
- public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] ISkin skin)
+ public LegacyBeatmapEncoder(IBeatmap beatmap, ISkin? skin)
{
this.beatmap = beatmap;
this.skin = skin;
@@ -180,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[TimingPoints]");
- SampleControlPoint lastRelevantSamplePoint = null;
- DifficultyControlPoint lastRelevantDifficultyPoint = null;
+ SampleControlPoint? lastRelevantSamplePoint = null;
+ DifficultyControlPoint? lastRelevantDifficultyPoint = null;
// In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats.
// In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored.
@@ -585,7 +581,7 @@ namespace osu.Game.Beatmaps.Formats
return type;
}
- private LegacySampleBank toLegacySampleBank(string sampleBank)
+ private LegacySampleBank toLegacySampleBank(string? sampleBank)
{
switch (sampleBank?.ToLowerInvariant())
{
@@ -603,7 +599,7 @@ namespace osu.Game.Beatmaps.Formats
}
}
- private int toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo)
+ private int toLegacyCustomSampleBank(HitSampleInfo? hitSampleInfo)
{
if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy)
return legacy.CustomSampleBank;
diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
index df5d3edb55..cf4700bf85 100644
--- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.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 System.Collections.Generic;
using System.IO;
@@ -19,10 +17,10 @@ namespace osu.Game.Beatmaps.Formats
{
public class LegacyStoryboardDecoder : LegacyDecoder
{
- private StoryboardSprite storyboardSprite;
- private CommandTimelineGroup timelineGroup;
+ private StoryboardSprite? storyboardSprite;
+ private CommandTimelineGroup? timelineGroup;
- private Storyboard storyboard;
+ private Storyboard storyboard = null!;
private readonly Dictionary variables = new Dictionary();
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..ece705f685 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)
@@ -67,7 +70,22 @@ namespace osu.Game.Database
hitObject.StartTime = Math.Floor(hitObject.StartTime);
- if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue;
+ if (hitObject is not IHasPath hasPath) continue;
+
+ // stable's hit object parsing expects the entire slider to use only one type of curve,
+ // and happens to use the last non-empty curve type read for the entire slider.
+ // this clear of the last control point type handles an edge case
+ // wherein the last control point of an otherwise-single-segment slider path has a different type than previous,
+ // which would lead to sliders being mangled when exported back to stable.
+ // normally, that would be handled by the `BezierConverter.ConvertToModernBezier()` call below,
+ // which outputs a slider path containing only Bezier control points,
+ // but a non-inherited last control point is (rightly) not considered to be starting a new segment,
+ // therefore it would fail to clear the `CountSegments() <= 1` check.
+ // by clearing explicitly we both fix the issue and avoid unnecessary conversions to Bezier.
+ if (hasPath.Path.ControlPoints.Count > 1)
+ hasPath.Path.ControlPoints[^1].Type = null;
+
+ if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue;
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
@@ -86,7 +104,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..cd97bb6430 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -82,8 +82,10 @@ 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.
+ /// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures.
///
- private const int schema_version = 32;
+ private const int schema_version = 34;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
@@ -771,6 +773,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 +787,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 +842,7 @@ namespace osu.Game.Database
break;
case 11:
+ {
string keyBindingClassName = getMappedOrOriginalName(typeof(RealmKeyBinding));
if (!migration.OldRealm.Schema.TryFindObjectSchema(keyBindingClassName, out _))
@@ -864,6 +869,7 @@ namespace osu.Game.Database
}
break;
+ }
case 14:
foreach (var beatmap in migration.NewRealm.All())
@@ -1012,6 +1018,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/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
index eb046932e6..2f2cb7e5f8 100644
--- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
+++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
@@ -40,8 +40,14 @@ namespace osu.Game.Graphics.UserInterface
AddInternal(hoverClickSounds = new HoverClickSounds());
updateTextColour();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
Item.Action.BindDisabledChanged(_ => updateState(), true);
+ FinishTransforms();
}
private void updateTextColour()
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/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..296232d9ea 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -3,33 +3,26 @@
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Input.Bindings
{
- public partial class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput
+ public partial class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput, IKeyBindingHandler
{
- private readonly Drawable? handler;
-
- private InputManager? parentInputManager;
+ private readonly IKeyBindingHandler? handler;
public GlobalActionContainer(OsuGameBase? game)
: base(matchingMode: KeyCombinationMatchingMode.Modifiers)
{
- if (game is IKeyBindingHandler)
- handler = game;
+ if (game is IKeyBindingHandler h)
+ handler = h;
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- parentInputManager = GetContainingInputManager();
- }
+ protected override bool Prioritised => true;
// IMPORTANT: Take care when changing order of the items in the enumerable.
// It is used to decide the order of precedence, with the earlier items having higher precedence.
@@ -105,6 +98,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 +110,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),
};
@@ -159,20 +154,9 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.F3, GlobalAction.MusicPlay)
};
- protected override IEnumerable KeyBindingInputQueue
- {
- get
- {
- // To ensure the global actions are handled with priority, this GlobalActionContainer is actually placed after game content.
- // It does not contain children as expected, so we need to forward the NonPositionalInputQueue from the parent input manager to correctly
- // allow the whole game to handle these actions.
+ public bool OnPressed(KeyBindingPressEvent e) => handler?.OnPressed(e) == true;
- // An eventual solution to this hack is to create localised action containers for individual components like SongSelect, but this will take some rearranging.
- var inputQueue = parentInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue;
-
- return handler != null ? inputQueue.Prepend(handler) : inputQueue;
- }
- }
+ public void OnReleased(KeyBindingReleaseEvent e) => handler?.OnReleased(e);
}
public enum GlobalAction
@@ -204,7 +188,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 +215,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 +242,6 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.PauseGameplay))]
PauseGameplay,
- // Editor
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSetupMode))]
EditorSetupMode,
@@ -285,7 +266,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 +358,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/Localisation/OnlinePlayStrings.cs b/osu.Game/Localisation/OnlinePlayStrings.cs
new file mode 100644
index 0000000000..1853cb753a
--- /dev/null
+++ b/osu.Game/Localisation/OnlinePlayStrings.cs
@@ -0,0 +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 osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class OnlinePlayStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.OnlinePlay";
+
+ ///
+ /// "Playlist durations longer than 2 weeks require an active osu!supporter tag."
+ ///
+ public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"Playlist durations longer than 2 weeks require an active osu!supporter tag.");
+
+ 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/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs
index 65aac723da..56f490cb21 100644
--- a/osu.Game/Online/Chat/MessageNotifier.cs
+++ b/osu.Game/Online/Chat/MessageNotifier.cs
@@ -182,8 +182,6 @@ namespace osu.Game.Online.Chat
private readonly Message message;
private readonly Channel channel;
- public override bool IsImportant => false;
-
[BackgroundDependencyLoader]
private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay)
{
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 1a40bb8e3d..c60bff9e4c 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -1025,7 +1025,7 @@ namespace osu.Game
loadComponentSingleFile(CreateHighPerformanceSession(), Add);
- loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add);
+ loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener());
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 6737caa5f9..75b46a0a4d 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -392,17 +392,18 @@ namespace osu.Game
{
SafeAreaOverrideEdges = SafeAreaOverrideEdges,
RelativeSizeAxes = Axes.Both,
- Child = CreateScalingContainer().WithChildren(new Drawable[]
+ Child = CreateScalingContainer().WithChild(globalBindings = new GlobalActionContainer(this)
{
- (GlobalCursorDisplay = new GlobalCursorDisplay
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both
- }).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor)
- {
- RelativeSizeAxes = Axes.Both
- }),
- // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything.
- globalBindings = new GlobalActionContainer(this)
+ (GlobalCursorDisplay = new GlobalCursorDisplay
+ {
+ RelativeSizeAxes = Axes.Both
+ }).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor)
+ {
+ RelativeSizeAxes = Axes.Both
+ }),
+ }
})
});
diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs
index c9d09848f8..b93d5f1e12 100644
--- a/osu.Game/Overlays/NotificationOverlay.cs
+++ b/osu.Game/Overlays/NotificationOverlay.cs
@@ -43,6 +43,9 @@ namespace osu.Game.Overlays
[Resolved]
private AudioManager audio { get; set; } = null!;
+ [Resolved]
+ private OsuGame? game { get; set; }
+
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
@@ -176,6 +179,12 @@ namespace osu.Game.Overlays
playDebouncedSample(notification.PopInSampleName);
+ if (notification.IsImportant)
+ {
+ game?.Window?.Flash();
+ notification.Closed += () => game?.Window?.CancelFlash();
+ }
+
if (State.Value == Visibility.Hidden)
{
notification.IsInToastTray = true;
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..34113285a4 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 .
///
@@ -196,13 +198,28 @@ namespace osu.Game.Rulesets.Objects
}
///
- /// Returns the progress values at which segments of the path end.
+ /// Returns the progress values at which (control point) segments of the path end.
+ /// Ranges from 0 (beginning of the path) to 1 (end of the path) to infinity (beyond the end of the path).
///
+ ///
+ /// truncates the progression values to [0,1],
+ /// so you can't use this method in conjunction with that one to retrieve the positions of segment ends beyond the end of the path.
+ ///
+ ///
+ ///
+ /// In case is less than ,
+ /// the last segment ends after the end of the path, hence it returns a value greater than 1.
+ ///
+ ///
+ /// In case is greater than ,
+ /// the last segment ends before the end of the path, hence it returns a value less than 1.
+ ///
+ ///
public IEnumerable GetSegmentEnds()
{
ensureValid();
- return segmentEnds.Select(i => cumulativeLength[i] / calculatedLength);
+ return segmentEndDistances.Select(d => d / Distance);
}
private void invalidate()
@@ -251,8 +268,11 @@ 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 +318,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 +347,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/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs
index 92a3b570fb..6c88f01249 100644
--- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs
+++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types;
@@ -25,6 +26,53 @@ namespace osu.Game.Rulesets.Objects
/// The .
/// The positional offset of the resulting path. It should be added to the start position of this path.
public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset)
+ {
+ var controlPoints = sliderPath.ControlPoints;
+
+ var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.Linear && p.Type is null).ToList();
+
+ // Inherited points after a linear point, as well as the first control point if it inherited,
+ // should be treated as linear points, so their types are temporarily changed to linear.
+ inheritedLinearPoints.ForEach(p => p.Type = PathType.Linear);
+
+ double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
+
+ // Remove segments after the end of the slider.
+ for (int numSegmentsToRemove = segmentEnds.Count(se => se >= 1) - 1; numSegmentsToRemove > 0 && controlPoints.Count > 0;)
+ {
+ if (controlPoints.Last().Type is not null)
+ {
+ numSegmentsToRemove--;
+ segmentEnds = segmentEnds[..^1];
+ }
+
+ controlPoints.RemoveAt(controlPoints.Count - 1);
+ }
+
+ // Restore original control point types.
+ inheritedLinearPoints.ForEach(p => p.Type = null);
+
+ // Recalculate middle perfect curve control points at the end of the slider path.
+ if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && segmentEnds.Any())
+ {
+ double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0;
+ double lastSegmentEnd = segmentEnds[^1];
+
+ var circleArcPath = new List();
+ sliderPath.GetPathToProgress(circleArcPath, lastSegmentStart / lastSegmentEnd, 1);
+
+ controlPoints[^2].Position = circleArcPath[circleArcPath.Count / 2];
+ }
+
+ sliderPath.reverseControlPoints(out positionalOffset);
+ }
+
+ ///
+ /// Reverses the order of the provided 's s.
+ ///
+ /// The .
+ /// The positional offset of the resulting path. It should be added to the start position of this path.
+ private static void reverseControlPoints(this SliderPath sliderPath, out Vector2 positionalOffset)
{
var points = sliderPath.ControlPoints.ToArray();
positionalOffset = sliderPath.PositionAt(1);
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index c6f4433824..2efea2105c 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Scoring
/// If this does not match ,
/// the total score has not yet been updated to reflect the current scoring values.
///
- /// See 's conversion logic.
+ /// See 's conversion logic.
///
///
/// This may not match the version stored in the replay files.
@@ -81,6 +81,15 @@ namespace osu.Game.Scoring
///
public long? LegacyTotalScore { get; set; }
+ ///
+ /// If background processing of this beatmap failed in some way, this flag will become true.
+ /// Should be used to ensure we don't repeatedly attempt to reprocess the same scores each startup even though we already know they will fail.
+ ///
+ ///
+ /// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur (missing beatmap file on disk).
+ ///
+ public bool BackgroundReprocessingFailed { get; set; }
+
public int MaxCombo { get; set; }
public double Accuracy { get; set; }
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/Loader.cs b/osu.Game/Screens/Loader.cs
index 372cfe748e..962c7d9d14 100644
--- a/osu.Game/Screens/Loader.cs
+++ b/osu.Game/Screens/Loader.cs
@@ -126,12 +126,9 @@ namespace osu.Game.Screens
private void load(ShaderManager manager)
{
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE));
- loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR));
-
+ loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2_NO_MASKING, FragmentShaderDescriptor.BLUR));
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
-
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"));
-
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
}
diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs
index a4d58e398a..07c06dcdb9 100644
--- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs
+++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs
@@ -2,10 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
+using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
@@ -13,6 +13,9 @@ namespace osu.Game.Screens.Menu
{
public partial class KiaiMenuFountains : BeatSyncedContainer
{
+ private StarFountain leftFountain = null!;
+ private StarFountain rightFountain = null!;
+
[BackgroundDependencyLoader]
private void load()
{
@@ -20,13 +23,13 @@ namespace osu.Game.Screens.Menu
Children = new[]
{
- new StarFountain
+ leftFountain = new StarFountain
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
X = 250,
},
- new StarFountain
+ rightFountain = new StarFountain
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
@@ -58,8 +61,25 @@ namespace osu.Game.Screens.Menu
if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500)
return;
- foreach (var fountain in Children.OfType())
- fountain.Shoot();
+ int direction = RNG.Next(-1, 2);
+
+ switch (direction)
+ {
+ case -1:
+ leftFountain.Shoot(1);
+ rightFountain.Shoot(-1);
+ break;
+
+ case 0:
+ leftFountain.Shoot(0);
+ rightFountain.Shoot(0);
+ break;
+
+ case 1:
+ leftFountain.Shoot(-1);
+ rightFountain.Shoot(1);
+ break;
+ }
lastTrigger = Clock.CurrentTime;
}
diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs
index 0d35f6e0e0..fd59ec3573 100644
--- a/osu.Game/Screens/Menu/StarFountain.cs
+++ b/osu.Game/Screens/Menu/StarFountain.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Screens.Menu
InternalChild = spewer = new StarFountainSpewer();
}
- public void Shoot() => spewer.Shoot();
+ public void Shoot(int direction) => spewer.Shoot(direction);
protected override void SkinChanged(ISkinSource skin)
{
@@ -81,10 +81,10 @@ namespace osu.Game.Screens.Menu
return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance);
}
- public void Shoot()
+ public void Shoot(int direction)
{
lastShootTime = Clock.CurrentTime;
- lastShootDirection = RNG.Next(-1, 2);
+ lastShootDirection = direction;
}
private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance);
diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs
index 05232fe0e2..916b799d50 100644
--- a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs
@@ -113,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected partial class Section : Container
{
- private readonly Container content;
+ private readonly ReverseChildIDFillFlowContainer content;
protected override Container Content => content;
@@ -135,10 +135,11 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12),
Text = title.ToUpperInvariant(),
},
- content = new Container
+ content = new ReverseChildIDFillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical
},
},
};
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/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
index e93f56c2e2..84e419d67a 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
@@ -23,6 +23,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osuTK;
+using osu.Game.Localisation;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
@@ -80,6 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private IBindable localUser = null!;
private readonly Room room;
+ private OsuSpriteText durationNoticeText = null!;
public MatchSettings(Room room)
{
@@ -141,14 +143,22 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
},
new Section("Duration")
{
- Child = new Container
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- Height = 40,
- Child = DurationField = new DurationDropdown
+ new Container
{
- RelativeSizeAxes = Axes.X
- }
+ RelativeSizeAxes = Axes.X,
+ Height = 40,
+ Child = DurationField = new DurationDropdown
+ {
+ RelativeSizeAxes = Axes.X
+ },
+ },
+ durationNoticeText = new OsuSpriteText
+ {
+ Alpha = 0,
+ Colour = colours.Yellow,
+ },
}
},
new Section("Allowed attempts (across all playlist items)")
@@ -305,6 +315,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true);
Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true);
+ DurationField.Current.BindValueChanged(duration =>
+ {
+ if (hasValidDuration)
+ durationNoticeText.Hide();
+ else
+ {
+ durationNoticeText.Show();
+ durationNoticeText.Text = OnlinePlayStrings.SupporterOnlyDurationNotice;
+ }
+ });
+
localUser = api.LocalUser.GetBoundCopy();
localUser.BindValueChanged(populateDurations, true);
@@ -314,6 +335,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private void populateDurations(ValueChangedEvent user)
{
+ // roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427)
+ // if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though.
+ const int days_in_month = 31;
+
DurationField.Items = new[]
{
TimeSpan.FromMinutes(30),
@@ -326,18 +351,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
TimeSpan.FromDays(3),
TimeSpan.FromDays(7),
TimeSpan.FromDays(14),
+ TimeSpan.FromDays(days_in_month),
+ TimeSpan.FromDays(days_in_month * 3),
};
-
- // TODO: show these in the interface at all times.
- if (user.NewValue.IsSupporter)
- {
- // roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427)
- // if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though.
- const int days_in_month = 31;
-
- DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month));
- DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month * 3));
- }
}
protected override void Update()
@@ -352,7 +368,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";
- private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0;
+ private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0
+ && hasValidDuration;
+
+ private bool hasValidDuration => DurationField.Current.Value <= TimeSpan.FromDays(14) || localUser.Value.IsSupporter;
private void apply()
{
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/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs
index ba392386de..61b0dc5aa1 100644
--- a/osu.Game/Skinning/ArgonSkin.cs
+++ b/osu.Game/Skinning/ArgonSkin.cs
@@ -232,6 +232,6 @@ namespace osu.Game.Skinning
}
private static Color4 getComboColour(IHasComboColours source, int colourIndex)
- => source.ComboColours[colourIndex % source.ComboColours.Count];
+ => source.ComboColours![colourIndex % source.ComboColours.Count];
}
}
diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs
index a68a7fd5b9..0957f60579 100644
--- a/osu.Game/Skinning/TrianglesSkin.cs
+++ b/osu.Game/Skinning/TrianglesSkin.cs
@@ -203,6 +203,6 @@ namespace osu.Game.Skinning
}
private static Color4 getComboColour(IHasComboColours source, int colourIndex)
- => source.ComboColours[colourIndex % source.ComboColours.Count];
+ => source.ComboColours![colourIndex % source.ComboColours.Count];
}
}
diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
index 37260b3b13..ffe40243ab 100644
--- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
@@ -57,7 +57,13 @@ namespace osu.Game.Tests.Visual
}
if (CreateNestedActionContainer)
- mainContent.Add(new GlobalActionContainer(null));
+ {
+ var globalActionContainer = new GlobalActionContainer(null)
+ {
+ Child = mainContent
+ };
+ mainContent = globalActionContainer;
+ }
base.Content.AddRange(new Drawable[]
{
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 dd6f9b3a96..4c3205178a 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,8 +36,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 0cf1fe9c28..fd1ba0f6d1 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -23,6 +23,6 @@
iossimulator-x64
-
+