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 - +