diff --git a/osu.Android.props b/osu.Android.props
index 9ad5946311..7060e88026 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 4554f8b83a..cce7907c6c 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -24,16 +24,13 @@
+
-
+
-
-
-
-
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 32e8ab5da7..64ded8e94f 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -45,6 +45,11 @@ namespace osu.Game.Rulesets.Catch.Replays
float positionChange = Math.Abs(lastPosition - h.EffectiveX);
double timeAvailable = h.StartTime - lastTime;
+ if (timeAvailable < 0)
+ {
+ return;
+ }
+
// So we can either make it there without a dash or not.
// If positionChange is 0, we don't need to move, so speedRequired should also be 0 (could be NaN if timeAvailable is 0 too)
// The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour.
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 56aedebed3..c58f703bef 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -243,7 +243,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.Update();
if (HandleUserInput)
- RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
+ {
+ bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime;
+ bool correctButtonPressed = (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
+
+ RotationTracker.Tracking = !Result.HasResult
+ && correctButtonPressed
+ && isValidSpinningTime;
+ }
if (spinningSample != null && spinnerFrequencyModulate)
spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress;
@@ -255,6 +262,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
+
SpmCounter.SetRotation(Result.RateAdjustedRotation);
updateBonusScore();
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
index e5952ecf97..69355f624b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
@@ -4,16 +4,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class SpinnerSpmCounter : Container
{
+ [Resolved]
+ private DrawableHitObject drawableSpinner { get; set; }
+
private readonly OsuSpriteText spmText;
public SpinnerSpmCounter()
@@ -38,6 +43,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ drawableSpinner.HitObjectApplied += resetState;
+ }
+
private double spm;
public double SpinsPerMinute
@@ -82,5 +93,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
}
+
+ private void resetState(DrawableHitObject hitObject)
+ {
+ SpinsPerMinute = 0;
+ records.Clear();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableSpinner != null)
+ drawableSpinner.HitObjectApplied -= resetState;
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
index 56a73ad7df..4006652bd5 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
@@ -1,11 +1,45 @@
// 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.Bindables;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDifficultyAdjust : ModDifficultyAdjust
{
+ [SettingSource("Scroll Speed", "Adjust a beatmap's set scroll speed", LAST_SETTING_ORDER + 1)]
+ public BindableNumber ScrollSpeed { get; } = new BindableFloat
+ {
+ Precision = 0.05f,
+ MinValue = 0.25f,
+ MaxValue = 4,
+ Default = 1,
+ Value = 1,
+ };
+
+ public override string SettingDescription
+ {
+ get
+ {
+ string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N1}";
+
+ return string.Join(", ", new[]
+ {
+ base.SettingDescription,
+ scrollSpeed
+ }.Where(s => !string.IsNullOrEmpty(s)));
+ }
+ }
+
+ protected override void ApplySettings(BeatmapDifficulty difficulty)
+ {
+ base.ApplySettings(difficulty);
+
+ ApplySetting(ScrollSpeed, scroll => difficulty.SliderMultiplier *= scroll);
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs
index d1ad4c9d8d..ad6fdf59e2 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.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.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Mods
@@ -8,5 +9,16 @@ namespace osu.Game.Rulesets.Taiko.Mods
public class TaikoModEasy : ModEasy
{
public override string Description => @"Beats move slower, and less accuracy required!";
+
+ ///
+ /// Multiplier factor added to the scrolling speed.
+ ///
+ private const double slider_multiplier = 0.8;
+
+ public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
+ {
+ base.ApplyToDifficulty(difficulty);
+ difficulty.SliderMultiplier *= slider_multiplier;
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs
index 49d225cdb5..a5a8b75f80 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.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.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Mods
@@ -9,5 +10,21 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public override double ScoreMultiplier => 1.06;
public override bool Ranked => true;
+
+ ///
+ /// Multiplier factor added to the scrolling speed.
+ ///
+ ///
+ /// This factor is made up of two parts: the base part (1.4) and the aspect ratio adjustment (4/3).
+ /// Stable applies the latter by dividing the width of the user's display by the width of a display with the same height, but 4:3 aspect ratio.
+ /// TODO: Revisit if taiko playfield ever changes away from a hard-coded 16:9 (see https://github.com/ppy/osu/issues/5685).
+ ///
+ private const double slider_multiplier = 1.4 * 4 / 3;
+
+ public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
+ {
+ base.ApplyToDifficulty(difficulty);
+ difficulty.SliderMultiplier *= slider_multiplier;
+ }
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
index 7bee580863..bcde899789 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
@@ -129,5 +129,25 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(3456, ((StoryboardSprite)background.Elements.Single()).InitialPosition.X);
}
}
+
+ [Test]
+ public void TestDecodeOutOfRangeLoopAnimationType()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("animation-types.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer foreground = storyboard.Layers.Single(l => l.Depth == 0);
+ Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[0]).LoopType);
+ Assert.AreEqual(AnimationLoopType.LoopOnce, ((StoryboardAnimation)foreground.Elements[1]).LoopType);
+ Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[2]).LoopType);
+ Assert.AreEqual(AnimationLoopType.LoopOnce, ((StoryboardAnimation)foreground.Elements[3]).LoopType);
+ Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[4]).LoopType);
+ Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[5]).LoopType);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs b/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs
new file mode 100644
index 0000000000..eef9582af9
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs
@@ -0,0 +1,62 @@
+// 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 NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Framework.Testing;
+using osu.Game.Screens.OnlinePlay;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.NonVisual
+{
+ [HeadlessTest]
+ public class OngoingOperationTrackerTest : OsuTestScene
+ {
+ private OngoingOperationTracker tracker;
+ private IBindable operationInProgress;
+
+ [SetUpSteps]
+ public void SetUp()
+ {
+ AddStep("create tracker", () => Child = tracker = new OngoingOperationTracker());
+ AddStep("bind to operation status", () => operationInProgress = tracker.InProgress.GetBoundCopy());
+ }
+
+ [Test]
+ public void TestOperationTracking()
+ {
+ IDisposable firstOperation = null;
+ IDisposable secondOperation = null;
+
+ AddStep("begin first operation", () => firstOperation = tracker.BeginOperation());
+ AddAssert("first operation in progress", () => operationInProgress.Value);
+
+ AddStep("cannot start another operation",
+ () => Assert.Throws(() => tracker.BeginOperation()));
+
+ AddStep("end first operation", () => firstOperation.Dispose());
+ AddAssert("first operation is ended", () => !operationInProgress.Value);
+
+ AddStep("start second operation", () => secondOperation = tracker.BeginOperation());
+ AddAssert("second operation in progress", () => operationInProgress.Value);
+
+ AddStep("dispose first operation again", () => firstOperation.Dispose());
+ AddAssert("second operation still in progress", () => operationInProgress.Value);
+
+ AddStep("dispose second operation", () => secondOperation.Dispose());
+ AddAssert("second operation is ended", () => !operationInProgress.Value);
+ }
+
+ [Test]
+ public void TestOperationDisposalAfterTracker()
+ {
+ IDisposable operation = null;
+
+ AddStep("begin operation", () => operation = tracker.BeginOperation());
+ AddStep("dispose tracker", () => tracker.Expire());
+ AddStep("end operation", () => operation.Dispose());
+ AddAssert("operation is ended", () => !operationInProgress.Value);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Resources/animation-types.osb b/osu.Game.Tests/Resources/animation-types.osb
new file mode 100644
index 0000000000..82233b7d30
--- /dev/null
+++ b/osu.Game.Tests/Resources/animation-types.osb
@@ -0,0 +1,9 @@
+osu file format v14
+
+[Events]
+Animation,Foreground,Centre,"forever-string.png",330,240,10,108,LoopForever
+Animation,Foreground,Centre,"once-string.png",330,240,10,108,LoopOnce
+Animation,Foreground,Centre,"forever-number.png",330,240,10,108,0
+Animation,Foreground,Centre,"once-number.png",330,240,10,108,1
+Animation,Foreground,Centre,"undefined-number.png",330,240,10,108,16
+Animation,Foreground,Centre,"omitted.png",330,240,10,108
diff --git a/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs b/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs
new file mode 100644
index 0000000000..4b9f2181dc
--- /dev/null
+++ b/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs
@@ -0,0 +1,113 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Audio.Track;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Tests.Rulesets.Mods
+{
+ [TestFixture]
+ public class ModTimeRampTest
+ {
+ private const double start_time = 1000;
+ private const double duration = 9000;
+
+ private TrackVirtual track;
+
+ [SetUp]
+ public void SetUp()
+ {
+ track = new TrackVirtual(20_000);
+ }
+
+ [TestCase(0, 1)]
+ [TestCase(start_time, 1)]
+ [TestCase(start_time + duration * ModTimeRamp.FINAL_RATE_PROGRESS / 2, 1.25)]
+ [TestCase(start_time + duration * ModTimeRamp.FINAL_RATE_PROGRESS, 1.5)]
+ [TestCase(start_time + duration, 1.5)]
+ [TestCase(15000, 1.5)]
+ public void TestModWindUp(double time, double expectedRate)
+ {
+ var beatmap = createSingleSpinnerBeatmap();
+ var mod = new ModWindUp();
+ mod.ApplyToBeatmap(beatmap);
+ mod.ApplyToTrack(track);
+
+ seekTrackAndUpdateMod(mod, time);
+
+ Assert.That(mod.SpeedChange.Value, Is.EqualTo(expectedRate));
+ }
+
+ [TestCase(0, 1)]
+ [TestCase(start_time, 1)]
+ [TestCase(start_time + duration * ModTimeRamp.FINAL_RATE_PROGRESS / 2, 0.75)]
+ [TestCase(start_time + duration * ModTimeRamp.FINAL_RATE_PROGRESS, 0.5)]
+ [TestCase(start_time + duration, 0.5)]
+ [TestCase(15000, 0.5)]
+ public void TestModWindDown(double time, double expectedRate)
+ {
+ var beatmap = createSingleSpinnerBeatmap();
+ var mod = new ModWindDown
+ {
+ FinalRate = { Value = 0.5 }
+ };
+ mod.ApplyToBeatmap(beatmap);
+ mod.ApplyToTrack(track);
+
+ seekTrackAndUpdateMod(mod, time);
+
+ Assert.That(mod.SpeedChange.Value, Is.EqualTo(expectedRate));
+ }
+
+ [TestCase(0, 1)]
+ [TestCase(start_time, 1)]
+ [TestCase(2 * start_time, 1.5)]
+ public void TestZeroDurationMap(double time, double expectedRate)
+ {
+ var beatmap = createSingleObjectBeatmap();
+ var mod = new ModWindUp();
+ mod.ApplyToBeatmap(beatmap);
+ mod.ApplyToTrack(track);
+
+ seekTrackAndUpdateMod(mod, time);
+
+ Assert.That(mod.SpeedChange.Value, Is.EqualTo(expectedRate));
+ }
+
+ private void seekTrackAndUpdateMod(ModTimeRamp mod, double time)
+ {
+ track.Seek(time);
+ // update the mod via a fake playfield to re-calculate the current rate.
+ mod.Update(null);
+ }
+
+ private static Beatmap createSingleSpinnerBeatmap()
+ {
+ return new Beatmap
+ {
+ HitObjects =
+ {
+ new Spinner
+ {
+ StartTime = start_time,
+ Duration = duration
+ }
+ }
+ };
+ }
+
+ private static Beatmap createSingleObjectBeatmap()
+ {
+ return new Beatmap
+ {
+ HitObjects =
+ {
+ new HitCircle { StartTime = start_time }
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
index 3adc1bd425..94a9fd7b35 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
@@ -5,6 +5,8 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osuTK;
@@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneEditorSummaryTimeline : EditorClockTestScene
{
+ [Cached(typeof(EditorBeatmap))]
+ private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs
new file mode 100644
index 0000000000..1544f8fd35
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs
@@ -0,0 +1,74 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Storyboards;
+using osu.Game.Storyboards.Drawables;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneStoryboardSamplePlayback : PlayerTestScene
+ {
+ private Storyboard storyboard;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ config.Set(OsuSetting.ShowStoryboard, true);
+
+ storyboard = new Storyboard();
+ var backgroundLayer = storyboard.GetLayer("Background");
+ backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -7000, volume: 20));
+ backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -5000, volume: 20));
+ backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20));
+ }
+
+ [Test]
+ public void TestStoryboardSamplesStopDuringPause()
+ {
+ checkForFirstSamplePlayback();
+
+ AddStep("player paused", () => Player.Pause());
+ AddAssert("player is currently paused", () => Player.GameplayClockContainer.IsPaused.Value);
+ AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying));
+
+ AddStep("player resume", () => Player.Resume());
+ AddUntilStep("any storyboard samples playing after resume", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
+ }
+
+ [Test]
+ public void TestStoryboardSamplesStopOnSkip()
+ {
+ checkForFirstSamplePlayback();
+
+ AddStep("skip intro", () => InputManager.Key(osuTK.Input.Key.Space));
+ AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying));
+
+ AddUntilStep("any storyboard samples playing after skip", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
+ }
+
+ private void checkForFirstSamplePlayback()
+ {
+ AddUntilStep("storyboard loaded", () => Player.Beatmap.Value.StoryboardLoaded);
+ AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
+ }
+
+ private IEnumerable allStoryboardSamples => Player.ChildrenOfType();
+
+ protected override bool AllowFail => false;
+
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false);
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) =>
+ new ClockBackedTestWorkingBeatmap(beatmap, storyboard ?? this.storyboard, Clock, Audio);
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
index 6cb1687d1f..1349264bf9 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
@@ -1,32 +1,81 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Overlays;
+using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays;
+using osu.Game.Overlays.BeatmapListing;
+using osu.Game.Rulesets;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneBeatmapListingOverlay : OsuTestScene
{
- protected override bool UseOnlineAPI => true;
+ private readonly List setsForResponse = new List();
- private readonly BeatmapListingOverlay overlay;
+ private BeatmapListingOverlay overlay;
- public TestSceneBeatmapListingOverlay()
+ [BackgroundDependencyLoader]
+ private void load()
{
- Add(overlay = new BeatmapListingOverlay());
+ Child = overlay = new BeatmapListingOverlay { State = { Value = Visibility.Visible } };
+
+ ((DummyAPIAccess)API).HandleRequest = req =>
+ {
+ if (req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)
+ {
+ searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse
+ {
+ BeatmapSets = setsForResponse,
+ });
+ }
+ };
}
[Test]
- public void TestShow()
+ public void TestNoBeatmapsPlaceholder()
{
- AddStep("Show", overlay.Show);
+ AddStep("fetch for 0 beatmaps", () => fetchFor());
+ AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
+
+ AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
+ AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType().Any());
+
+ AddStep("fetch for 0 beatmaps", () => fetchFor());
+ AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
+
+ // fetch once more to ensure nothing happens in displaying placeholder again when it already is present.
+ AddStep("fetch for 0 beatmaps again", () => fetchFor());
+ AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
}
- [Test]
- public void TestHide()
+ private void fetchFor(params BeatmapSetInfo[] beatmaps)
{
- AddStep("Hide", overlay.Hide);
+ setsForResponse.Clear();
+ setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b)));
+
+ // trigger arbitrary change for fetching.
+ overlay.ChildrenOfType().Single().Query.TriggerChange();
+ }
+
+ private class TestAPIBeatmapSet : APIBeatmapSet
+ {
+ private readonly BeatmapSetInfo beatmapSet;
+
+ public TestAPIBeatmapSet(BeatmapSetInfo beatmapSet)
+ {
+ this.beatmapSet = beatmapSet;
+ }
+
+ public override BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) => beatmapSet;
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs
new file mode 100644
index 0000000000..fe1701a554
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Overlays;
+using NUnit.Framework;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [Description("uses online API")]
+ public class TestSceneOnlineBeatmapListingOverlay : OsuTestScene
+ {
+ protected override bool UseOnlineAPI => true;
+
+ private readonly BeatmapListingOverlay overlay;
+
+ public TestSceneOnlineBeatmapListingOverlay()
+ {
+ Add(overlay = new BeatmapListingOverlay());
+ }
+
+ [Test]
+ public void TestShow()
+ {
+ AddStep("Show", overlay.Show);
+ }
+
+ [Test]
+ public void TestHide()
+ {
+ AddStep("Hide", overlay.Hide);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
index 9bb29541ec..e9e826e62f 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
@@ -7,6 +7,8 @@ using osu.Game.Overlays.Comments;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Allocation;
using osu.Game.Overlays;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Containers;
namespace osu.Game.Tests.Visual.Online
{
@@ -16,13 +18,33 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
- private VotePill votePill;
+ [Cached]
+ private LoginOverlay login;
+
+ private TestPill votePill;
+ private readonly Container pillContainer;
+
+ public TestSceneVotePill()
+ {
+ AddRange(new Drawable[]
+ {
+ pillContainer = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Both
+ },
+ login = new LoginOverlay()
+ });
+ }
[Test]
public void TestUserCommentPill()
{
+ AddStep("Hide login overlay", () => login.Hide());
AddStep("Log in", logIn);
AddStep("User comment", () => addVotePill(getUserComment()));
+ AddAssert("Background is transparent", () => votePill.Background.Alpha == 0);
AddStep("Click", () => votePill.Click());
AddAssert("Not loading", () => !votePill.IsLoading);
}
@@ -30,8 +52,10 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestRandomCommentPill()
{
+ AddStep("Hide login overlay", () => login.Hide());
AddStep("Log in", logIn);
AddStep("Random comment", () => addVotePill(getRandomComment()));
+ AddAssert("Background is visible", () => votePill.Background.Alpha == 1);
AddStep("Click", () => votePill.Click());
AddAssert("Loading", () => votePill.IsLoading);
}
@@ -39,10 +63,11 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestOfflineRandomCommentPill()
{
+ AddStep("Hide login overlay", () => login.Hide());
AddStep("Log out", API.Logout);
AddStep("Random comment", () => addVotePill(getRandomComment()));
AddStep("Click", () => votePill.Click());
- AddAssert("Not loading", () => !votePill.IsLoading);
+ AddAssert("Login overlay is visible", () => login.State.Value == Visibility.Visible);
}
private void logIn() => API.Login("localUser", "password");
@@ -63,12 +88,22 @@ namespace osu.Game.Tests.Visual.Online
private void addVotePill(Comment comment)
{
- Clear();
- Add(votePill = new VotePill(comment)
+ pillContainer.Clear();
+ pillContainer.Child = votePill = new TestPill(comment)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- });
+ };
+ }
+
+ private class TestPill : VotePill
+ {
+ public new Box Background => base.Background;
+
+ public TestPill(Comment comment)
+ : base(comment)
+ {
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs
new file mode 100644
index 0000000000..5c2e6e457d
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs
@@ -0,0 +1,122 @@
+// 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;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.Containers;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneSectionsContainer : OsuManualInputManagerTestScene
+ {
+ private readonly SectionsContainer container;
+ private float custom;
+ private const float header_height = 100;
+
+ public TestSceneSectionsContainer()
+ {
+ container = new SectionsContainer
+ {
+ RelativeSizeAxes = Axes.Y,
+ Width = 300,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ FixedHeader = new Box
+ {
+ Alpha = 0.5f,
+ Width = 300,
+ Height = header_height,
+ Colour = Color4.Red
+ }
+ };
+ container.SelectedSection.ValueChanged += section =>
+ {
+ if (section.OldValue != null)
+ section.OldValue.Selected = false;
+ if (section.NewValue != null)
+ section.NewValue.Selected = true;
+ };
+ Add(container);
+ }
+
+ [Test]
+ public void TestSelection()
+ {
+ AddStep("clear", () => container.Clear());
+ AddStep("add 1/8th", () => append(1 / 8.0f));
+ AddStep("add third", () => append(1 / 3.0f));
+ AddStep("add half", () => append(1 / 2.0f));
+ AddStep("add full", () => append(1));
+ AddSliderStep("set custom", 0.1f, 1.1f, 0.5f, i => custom = i);
+ AddStep("add custom", () => append(custom));
+ AddStep("scroll to previous", () => container.ScrollTo(
+ container.Children.Reverse().SkipWhile(s => s != container.SelectedSection.Value).Skip(1).FirstOrDefault() ?? container.Children.First()
+ ));
+ AddStep("scroll to next", () => container.ScrollTo(
+ container.Children.SkipWhile(s => s != container.SelectedSection.Value).Skip(1).FirstOrDefault() ?? container.Children.Last()
+ ));
+ AddStep("scroll up", () => triggerUserScroll(1));
+ AddStep("scroll down", () => triggerUserScroll(-1));
+ }
+
+ [Test]
+ public void TestCorrectSectionSelected()
+ {
+ const int sections_count = 11;
+ float[] alternating = { 0.07f, 0.33f, 0.16f, 0.33f };
+ AddStep("clear", () => container.Clear());
+ AddStep("fill with sections", () =>
+ {
+ for (int i = 0; i < sections_count; i++)
+ append(alternating[i % alternating.Length]);
+ });
+
+ void step(int scrollIndex)
+ {
+ AddStep($"scroll to section {scrollIndex + 1}", () => container.ScrollTo(container.Children[scrollIndex]));
+ AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[scrollIndex]);
+ }
+
+ for (int i = 1; i < sections_count; i++)
+ step(i);
+ for (int i = sections_count - 2; i >= 0; i--)
+ step(i);
+
+ AddStep("scroll almost to end", () => container.ScrollTo(container.Children[sections_count - 2]));
+ AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[sections_count - 2]);
+ AddStep("scroll down", () => triggerUserScroll(-1));
+ AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[sections_count - 1]);
+ }
+
+ private static readonly ColourInfo selected_colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Gold);
+ private static readonly ColourInfo default_colour = ColourInfo.GradientVertical(Color4.White, Color4.DarkGray);
+
+ private void append(float multiplier)
+ {
+ container.Add(new TestSection
+ {
+ Width = 300,
+ Height = (container.ChildSize.Y - header_height) * multiplier,
+ Colour = default_colour
+ });
+ }
+
+ private void triggerUserScroll(float direction)
+ {
+ InputManager.MoveMouseTo(container);
+ InputManager.ScrollVerticalBy(direction);
+ }
+
+ private class TestSection : Box
+ {
+ public bool Selected
+ {
+ set => Colour = value ? selected_colour : default_colour;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs
new file mode 100644
index 0000000000..b4d9fa4222
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs
@@ -0,0 +1,60 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Rulesets;
+using osu.Game.Tournament.Components;
+
+namespace osu.Game.Tournament.Tests.Components
+{
+ public class TestSceneTournamentModDisplay : TournamentTestScene
+ {
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ private FillFlowContainer fillFlow;
+
+ private BeatmapInfo beatmap;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = 490154 });
+ req.Success += success;
+ api.Queue(req);
+
+ Add(fillFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Direction = FillDirection.Full,
+ Spacing = new osuTK.Vector2(10)
+ });
+ }
+
+ private void success(APIBeatmap apiBeatmap)
+ {
+ beatmap = apiBeatmap.ToBeatmap(rulesets);
+ var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods();
+
+ foreach (var mod in mods)
+ {
+ fillFlow.Add(new TournamentBeatmapPanel(beatmap, mod.Acronym)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs
index b240ef3ae5..0da8d1eb4a 100644
--- a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs
@@ -1,6 +1,8 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Tournament.Components;
@@ -16,5 +18,23 @@ namespace osu.Game.Tournament.Tests.Screens
Add(new TourneyVideo("main") { RelativeSizeAxes = Axes.Both });
Add(new ScheduleScreen());
}
+
+ [Test]
+ public void TestCurrentMatchTime()
+ {
+ setMatchDate(TimeSpan.FromDays(-1));
+ setMatchDate(TimeSpan.FromSeconds(5));
+ setMatchDate(TimeSpan.FromMinutes(4));
+ setMatchDate(TimeSpan.FromHours(3));
+ }
+
+ private void setMatchDate(TimeSpan relativeTime)
+ // Humanizer cannot handle negative timespans.
+ => AddStep($"start time is {relativeTime}", () =>
+ {
+ var match = CreateSampleMatch();
+ match.Date.Value = DateTimeOffset.Now + relativeTime;
+ Ladder.CurrentMatch.Value = match;
+ });
}
}
diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
index 477bf4bd63..d1197b1a61 100644
--- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
+++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
@@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
@@ -23,7 +22,7 @@ namespace osu.Game.Tournament.Components
public class TournamentBeatmapPanel : CompositeDrawable
{
public readonly BeatmapInfo Beatmap;
- private readonly string mods;
+ private readonly string mod;
private const float horizontal_padding = 10;
private const float vertical_padding = 10;
@@ -33,12 +32,12 @@ namespace osu.Game.Tournament.Components
private readonly Bindable currentMatch = new Bindable();
private Box flash;
- public TournamentBeatmapPanel(BeatmapInfo beatmap, string mods = null)
+ public TournamentBeatmapPanel(BeatmapInfo beatmap, string mod = null)
{
if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
Beatmap = beatmap;
- this.mods = mods;
+ this.mod = mod;
Width = 400;
Height = HEIGHT;
}
@@ -122,23 +121,15 @@ namespace osu.Game.Tournament.Components
},
});
- if (!string.IsNullOrEmpty(mods))
+ if (!string.IsNullOrEmpty(mod))
{
- AddInternal(new Container
+ AddInternal(new TournamentModIcon(mod)
{
- RelativeSizeAxes = Axes.Y,
- Width = 60,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding(10),
- Child = new Sprite
- {
- FillMode = FillMode.Fit,
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- Texture = textures.Get($"mods/{mods}"),
- }
+ Width = 60,
+ RelativeSizeAxes = Axes.Y,
});
}
}
diff --git a/osu.Game.Tournament/Components/TournamentModIcon.cs b/osu.Game.Tournament/Components/TournamentModIcon.cs
new file mode 100644
index 0000000000..43ac92d285
--- /dev/null
+++ b/osu.Game.Tournament/Components/TournamentModIcon.cs
@@ -0,0 +1,65 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.UI;
+using osu.Game.Tournament.Models;
+using osuTK;
+
+namespace osu.Game.Tournament.Components
+{
+ ///
+ /// Mod icon displayed in tournament usages, allowing user overridden graphics.
+ ///
+ public class TournamentModIcon : CompositeDrawable
+ {
+ private readonly string modAcronym;
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ public TournamentModIcon(string modAcronym)
+ {
+ this.modAcronym = modAcronym;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures, LadderInfo ladderInfo)
+ {
+ var customTexture = textures.Get($"mods/{modAcronym}");
+
+ if (customTexture != null)
+ {
+ AddInternal(new Sprite
+ {
+ FillMode = FillMode.Fit,
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Texture = customTexture
+ });
+
+ return;
+ }
+
+ var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0);
+ var modIcon = ruleset?.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == modAcronym);
+
+ if (modIcon == null)
+ return;
+
+ AddInternal(new ModIcon(modIcon, false)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(0.5f)
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
index 88289ad6bd..c1d8c8ddd3 100644
--- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
+++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
@@ -192,12 +192,7 @@ namespace osu.Game.Tournament.Screens.Schedule
Origin = Anchor.CentreLeft,
Children = new Drawable[]
{
- new TournamentSpriteText
- {
- Text = "Starting ",
- Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
- },
- new DrawableDate(match.NewValue.Date.Value)
+ new ScheduleMatchDate(match.NewValue.Date.Value)
{
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
}
@@ -251,6 +246,18 @@ namespace osu.Game.Tournament.Screens.Schedule
}
}
+ public class ScheduleMatchDate : DrawableDate
+ {
+ public ScheduleMatchDate(DateTimeOffset date, float textSize = OsuFont.DEFAULT_FONT_SIZE, bool italic = true)
+ : base(date, textSize, italic)
+ {
+ }
+
+ protected override string Format() => Date < DateTimeOffset.Now
+ ? $"Started {base.Format()}"
+ : $"Starting {base.Format()}";
+ }
+
public class ScheduleContainer : Container
{
protected override Container Content => content;
diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs
index be2006e67a..5435e86dfd 100644
--- a/osu.Game/Beatmaps/Beatmap.cs
+++ b/osu.Game/Beatmaps/Beatmap.cs
@@ -50,15 +50,7 @@ namespace osu.Game.Beatmaps
IBeatmap IBeatmap.Clone() => Clone();
- public Beatmap Clone()
- {
- var clone = (Beatmap)MemberwiseClone();
-
- clone.ControlPointInfo = ControlPointInfo.CreateCopy();
- // todo: deep clone other elements as required.
-
- return clone;
- }
+ public Beatmap Clone() => (Beatmap)MemberwiseClone();
}
public class Beatmap : Beatmap
diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
index e90ccbb805..7c4b344c9e 100644
--- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
@@ -7,7 +7,6 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Dapper;
using Microsoft.Data.Sqlite;
using osu.Framework.Development;
using osu.Framework.IO.Network;
@@ -154,20 +153,31 @@ namespace osu.Game.Beatmaps
{
using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online")))
{
- var found = db.QuerySingleOrDefault(
- "SELECT * FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path", beatmap);
+ db.Open();
- if (found != null)
+ using (var cmd = db.CreateCommand())
{
- var status = (BeatmapSetOnlineStatus)found.approved;
+ cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path";
- beatmap.Status = status;
- beatmap.BeatmapSet.Status = status;
- beatmap.BeatmapSet.OnlineBeatmapSetID = found.beatmapset_id;
- beatmap.OnlineBeatmapID = found.beatmap_id;
+ cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash));
+ cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value));
+ cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path));
- LogForModel(set, $"Cached local retrieval for {beatmap}.");
- return true;
+ using (var reader = cmd.ExecuteReader())
+ {
+ if (reader.Read())
+ {
+ var status = (BeatmapSetOnlineStatus)reader.GetByte(2);
+
+ beatmap.Status = status;
+ beatmap.BeatmapSet.Status = status;
+ beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0);
+ beatmap.OnlineBeatmapID = reader.GetInt32(1);
+
+ LogForModel(set, $"Cached local retrieval for {beatmap}.");
+ return true;
+ }
+ }
}
}
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
index 9a244c8bb2..b9bf6823b5 100644
--- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
@@ -139,7 +139,7 @@ namespace osu.Game.Beatmaps.Formats
// this is random as hell but taken straight from osu-stable.
frameDelay = Math.Round(0.015 * frameDelay) * 1.186 * (1000 / 60f);
- var loopType = split.Length > 8 ? (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), split[8]) : AnimationLoopType.LoopForever;
+ var loopType = split.Length > 8 ? parseAnimationLoopType(split[8]) : AnimationLoopType.LoopForever;
storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType);
storyboard.GetLayer(layer).Add(storyboardSprite);
break;
@@ -341,6 +341,12 @@ namespace osu.Game.Beatmaps.Formats
}
}
+ private AnimationLoopType parseAnimationLoopType(string value)
+ {
+ var parsed = (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), value);
+ return Enum.IsDefined(typeof(AnimationLoopType), parsed) ? parsed : AnimationLoopType.LoopForever;
+ }
+
private void handleVariables(string line)
{
var pair = SplitKeyVal(line, '=');
diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs
index 8f27e0b0e9..7dd85e1232 100644
--- a/osu.Game/Beatmaps/IBeatmap.cs
+++ b/osu.Game/Beatmaps/IBeatmap.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps
///
/// The control points in this beatmap.
///
- ControlPointInfo ControlPointInfo { get; }
+ ControlPointInfo ControlPointInfo { get; set; }
///
/// The breaks in this beatmap.
diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs
deleted file mode 100644
index 4138c2757a..0000000000
--- a/osu.Game/Extensions/TaskExtensions.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable enable
-
-using System;
-using System.Threading.Tasks;
-using osu.Framework.Extensions.ExceptionExtensions;
-using osu.Framework.Logging;
-
-namespace osu.Game.Extensions
-{
- public static class TaskExtensions
- {
- ///
- /// Denote a task which is to be run without local error handling logic, where failure is not catastrophic.
- /// Avoids unobserved exceptions from being fired.
- ///
- /// The task.
- ///
- /// Whether errors should be logged as errors visible to users, or as debug messages.
- /// Logging as debug will essentially silence the errors on non-release builds.
- ///
- public static void CatchUnobservedExceptions(this Task task, bool logAsError = false)
- {
- task.ContinueWith(t =>
- {
- Exception? exception = t.Exception?.AsSingular();
- if (logAsError)
- Logger.Error(exception, $"Error running task: {exception?.Message ?? "(unknown)"}", LoggingTarget.Runtime, true);
- else
- Logger.Log($"Error running task: {exception}", LoggingTarget.Runtime, LogLevel.Debug);
- }, TaskContinuationOptions.NotOnRanToCompletion);
- }
- }
-}
diff --git a/osu.Game/Graphics/Containers/ParallaxContainer.cs b/osu.Game/Graphics/Containers/ParallaxContainer.cs
index 4cd3934cde..b501e68ba1 100644
--- a/osu.Game/Graphics/Containers/ParallaxContainer.cs
+++ b/osu.Game/Graphics/Containers/ParallaxContainer.cs
@@ -24,6 +24,10 @@ namespace osu.Game.Graphics.Containers
private Bindable parallaxEnabled;
+ private const float parallax_duration = 100;
+
+ private bool firstUpdate = true;
+
public ParallaxContainer()
{
RelativeSizeAxes = Axes.Both;
@@ -60,17 +64,27 @@ namespace osu.Game.Graphics.Containers
input = GetContainingInputManager();
}
- private bool firstUpdate = true;
-
protected override void Update()
{
base.Update();
if (parallaxEnabled.Value)
{
- Vector2 offset = (input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.Position) - DrawSize / 2) * ParallaxAmount;
+ Vector2 offset = Vector2.Zero;
- const float parallax_duration = 100;
+ if (input.CurrentState.Mouse != null)
+ {
+ var sizeDiv2 = DrawSize / 2;
+
+ Vector2 relativeAmount = ToLocalSpace(input.CurrentState.Mouse.Position) - sizeDiv2;
+
+ const float base_factor = 0.999f;
+
+ relativeAmount.X = (float)(Math.Sign(relativeAmount.X) * Interpolation.Damp(0, 1, base_factor, Math.Abs(relativeAmount.X)));
+ relativeAmount.Y = (float)(Math.Sign(relativeAmount.Y) * Interpolation.Damp(0, 1, base_factor, Math.Abs(relativeAmount.Y)));
+
+ offset = relativeAmount * sizeDiv2 * ParallaxAmount;
+ }
double elapsed = Math.Clamp(Clock.ElapsedFrameTime, 0, parallax_duration);
diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs
index 81968de304..8ab146efe7 100644
--- a/osu.Game/Graphics/Containers/SectionsContainer.cs
+++ b/osu.Game/Graphics/Containers/SectionsContainer.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@@ -9,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
+using osu.Framework.Utils;
namespace osu.Game.Graphics.Containers
{
@@ -20,6 +22,7 @@ namespace osu.Game.Graphics.Containers
where T : Drawable
{
public Bindable SelectedSection { get; } = new Bindable();
+ private Drawable lastClickedSection;
public Drawable ExpandableHeader
{
@@ -36,7 +39,7 @@ namespace osu.Game.Graphics.Containers
if (value == null) return;
AddInternal(expandableHeader);
- lastKnownScroll = float.NaN;
+ lastKnownScroll = null;
}
}
@@ -52,7 +55,7 @@ namespace osu.Game.Graphics.Containers
if (value == null) return;
AddInternal(fixedHeader);
- lastKnownScroll = float.NaN;
+ lastKnownScroll = null;
}
}
@@ -71,7 +74,7 @@ namespace osu.Game.Graphics.Containers
footer.Anchor |= Anchor.y2;
footer.Origin |= Anchor.y2;
scrollContainer.Add(footer);
- lastKnownScroll = float.NaN;
+ lastKnownScroll = null;
}
}
@@ -89,21 +92,26 @@ namespace osu.Game.Graphics.Containers
headerBackgroundContainer.Add(headerBackground);
- lastKnownScroll = float.NaN;
+ lastKnownScroll = null;
}
}
protected override Container Content => scrollContentContainer;
- private readonly OsuScrollContainer scrollContainer;
+ private readonly UserTrackingScrollContainer scrollContainer;
private readonly Container headerBackgroundContainer;
private readonly MarginPadding originalSectionsMargin;
private Drawable expandableHeader, fixedHeader, footer, headerBackground;
private FlowContainer scrollContentContainer;
- private float headerHeight, footerHeight;
+ private float? headerHeight, footerHeight;
- private float lastKnownScroll;
+ private float? lastKnownScroll;
+
+ ///
+ /// The percentage of the container to consider the centre-point for deciding the active section (and scrolling to a requested section).
+ ///
+ private const float scroll_y_centre = 0.1f;
public SectionsContainer()
{
@@ -128,18 +136,24 @@ namespace osu.Game.Graphics.Containers
public override void Add(T drawable)
{
base.Add(drawable);
- lastKnownScroll = float.NaN;
- headerHeight = float.NaN;
- footerHeight = float.NaN;
+
+ Debug.Assert(drawable != null);
+
+ lastKnownScroll = null;
+ headerHeight = null;
+ footerHeight = null;
}
- public void ScrollTo(Drawable section) =>
- scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0));
+ public void ScrollTo(Drawable section)
+ {
+ lastClickedSection = section;
+ scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - scrollContainer.DisplayableContent * scroll_y_centre - (FixedHeader?.BoundingBox.Height ?? 0));
+ }
public void ScrollToTop() => scrollContainer.ScrollTo(0);
[NotNull]
- protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer();
+ protected virtual UserTrackingScrollContainer CreateScrollContainer() => new UserTrackingScrollContainer();
[NotNull]
protected virtual FlowContainer CreateScrollContentContainer() =>
@@ -156,7 +170,7 @@ namespace osu.Game.Graphics.Containers
if (source == InvalidationSource.Child && (invalidation & Invalidation.DrawSize) != 0)
{
- lastKnownScroll = -1;
+ lastKnownScroll = null;
result = true;
}
@@ -167,7 +181,10 @@ namespace osu.Game.Graphics.Containers
{
base.UpdateAfterChildren();
- float headerH = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0);
+ float fixedHeaderSize = FixedHeader?.LayoutSize.Y ?? 0;
+ float expandableHeaderSize = ExpandableHeader?.LayoutSize.Y ?? 0;
+
+ float headerH = expandableHeaderSize + fixedHeaderSize;
float footerH = Footer?.LayoutSize.Y ?? 0;
if (headerH != headerHeight || footerH != footerHeight)
@@ -183,28 +200,39 @@ namespace osu.Game.Graphics.Containers
{
lastKnownScroll = currentScroll;
+ // reset last clicked section because user started scrolling themselves
+ if (scrollContainer.UserScrolling)
+ lastClickedSection = null;
+
if (ExpandableHeader != null && FixedHeader != null)
{
- float offset = Math.Min(ExpandableHeader.LayoutSize.Y, currentScroll);
+ float offset = Math.Min(expandableHeaderSize, currentScroll);
ExpandableHeader.Y = -offset;
- FixedHeader.Y = -offset + ExpandableHeader.LayoutSize.Y;
+ FixedHeader.Y = -offset + expandableHeaderSize;
}
- headerBackgroundContainer.Height = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0);
+ headerBackgroundContainer.Height = expandableHeaderSize + fixedHeaderSize;
headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0;
- float scrollOffset = FixedHeader?.LayoutSize.Y ?? 0;
- Func diff = section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset;
+ var smallestSectionHeight = Children.Count > 0 ? Children.Min(d => d.Height) : 0;
- if (scrollContainer.IsScrolledToEnd())
- {
- SelectedSection.Value = Children.LastOrDefault();
- }
+ // scroll offset is our fixed header height if we have it plus 10% of content height
+ // plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards
+ // but the 5% can't be bigger than our smallest section height, otherwise it won't get selected correctly
+ float selectionLenienceAboveSection = Math.Min(smallestSectionHeight / 2.0f, scrollContainer.DisplayableContent * 0.05f);
+
+ float scrollCentre = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_y_centre + selectionLenienceAboveSection;
+
+ if (Precision.AlmostBigger(0, scrollContainer.Current))
+ SelectedSection.Value = lastClickedSection as T ?? Children.FirstOrDefault();
+ else if (Precision.AlmostBigger(scrollContainer.Current, scrollContainer.ScrollableExtent))
+ SelectedSection.Value = lastClickedSection as T ?? Children.LastOrDefault();
else
{
- SelectedSection.Value = Children.TakeWhile(section => diff(section) <= 0).LastOrDefault()
- ?? Children.FirstOrDefault();
+ SelectedSection.Value = Children
+ .TakeWhile(section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollCentre <= 0)
+ .LastOrDefault() ?? Children.FirstOrDefault();
}
}
}
@@ -214,8 +242,9 @@ namespace osu.Game.Graphics.Containers
if (!Children.Any()) return;
var newMargin = originalSectionsMargin;
- newMargin.Top += headerHeight;
- newMargin.Bottom += footerHeight;
+
+ newMargin.Top += (headerHeight ?? 0);
+ newMargin.Bottom += (footerHeight ?? 0);
scrollContentContainer.Margin = newMargin;
}
diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs
new file mode 100644
index 0000000000..b8ce34b204
--- /dev/null
+++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs
@@ -0,0 +1,49 @@
+// 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.Graphics;
+
+namespace osu.Game.Graphics.Containers
+{
+ public class UserTrackingScrollContainer : UserTrackingScrollContainer
+ {
+ public UserTrackingScrollContainer()
+ {
+ }
+
+ public UserTrackingScrollContainer(Direction direction)
+ : base(direction)
+ {
+ }
+ }
+
+ public class UserTrackingScrollContainer : OsuScrollContainer
+ where T : Drawable
+ {
+ ///
+ /// Whether the last scroll event was user triggered, directly on the scroll container.
+ ///
+ public bool UserScrolling { get; private set; }
+
+ public UserTrackingScrollContainer()
+ {
+ }
+
+ public UserTrackingScrollContainer(Direction direction)
+ : base(direction)
+ {
+ }
+
+ protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
+ {
+ UserScrolling = true;
+ base.OnUserScroll(value, animated, distanceDecay);
+ }
+
+ public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null)
+ {
+ UserScrolling = false;
+ base.ScrollTo(value, animated, distanceDecay);
+ }
+ }
+}
diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs
index c8b76b9685..69ce3825ee 100644
--- a/osu.Game/Online/API/APIMod.cs
+++ b/osu.Game/Online/API/APIMod.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Humanizer;
+using MessagePack;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Configuration;
@@ -13,16 +14,20 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Online.API
{
+ [MessagePackObject]
public class APIMod : IMod
{
[JsonProperty("acronym")]
+ [Key(0)]
public string Acronym { get; set; }
[JsonProperty("settings")]
+ [Key(1)]
public Dictionary Settings { get; set; } = new Dictionary();
[JsonConstructor]
- private APIMod()
+ [SerializationConstructor]
+ public APIMod()
{
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
index bd1800e9f7..45d9c9405f 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"beatmaps")]
private IEnumerable beatmaps { get; set; }
- public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets)
+ public virtual BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets)
{
var beatmapSet = new BeatmapSetInfo
{
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index ecf314c1e5..572b3bbdf2 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
+using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
@@ -66,13 +67,19 @@ namespace osu.Game.Online.Multiplayer
if (connection != null)
return;
- connection = new HubConnectionBuilder()
- .WithUrl(endpoint, options =>
- {
- options.Headers.Add("Authorization", $"Bearer {api.AccessToken}");
- })
- .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; })
- .Build();
+ var builder = new HubConnectionBuilder()
+ .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); });
+
+ if (RuntimeInfo.SupportsJIT)
+ builder.AddMessagePackProtocol();
+ else
+ {
+ // eventually we will precompile resolvers for messagepack, but this isn't working currently
+ // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
+ builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
+ }
+
+ connection = builder.Build();
// this is kind of SILLY
// https://github.com/dotnet/aspnetcore/issues/15198
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
index 12fcf25ace..c5fa6253ed 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
+using MessagePack;
using Newtonsoft.Json;
namespace osu.Game.Online.Multiplayer
@@ -13,35 +14,42 @@ namespace osu.Game.Online.Multiplayer
/// A multiplayer room.
///
[Serializable]
+ [MessagePackObject]
public class MultiplayerRoom
{
///
/// The ID of the room, used for database persistence.
///
+ [Key(0)]
public readonly long RoomID;
///
/// The current state of the room (ie. whether it is in progress or otherwise).
///
+ [Key(1)]
public MultiplayerRoomState State { get; set; }
///
/// All currently enforced game settings for this room.
///
+ [Key(2)]
public MultiplayerRoomSettings Settings { get; set; } = new MultiplayerRoomSettings();
///
/// All users currently in this room.
///
+ [Key(3)]
public List Users { get; set; } = new List();
///
/// The host of this room, in control of changing room settings.
///
+ [Key(4)]
public MultiplayerRoomUser? Host { get; set; }
[JsonConstructor]
- public MultiplayerRoom(in long roomId)
+ [SerializationConstructor]
+ public MultiplayerRoom(long roomId)
{
RoomID = roomId;
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
index ad624f18ed..a25c332b47 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
@@ -7,22 +7,29 @@ using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
+using MessagePack;
using osu.Game.Online.API;
namespace osu.Game.Online.Multiplayer
{
[Serializable]
+ [MessagePackObject]
public class MultiplayerRoomSettings : IEquatable
{
+ [Key(0)]
public int BeatmapID { get; set; }
+ [Key(1)]
public int RulesetID { get; set; }
+ [Key(2)]
public string BeatmapChecksum { get; set; } = string.Empty;
+ [Key(3)]
public string Name { get; set; } = "Unnamed room";
[NotNull]
+ [Key(4)]
public IEnumerable Mods { get; set; } = Enumerable.Empty();
[NotNull]
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
index 7de24826dd..3271133bcd 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
+using MessagePack;
using Newtonsoft.Json;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@@ -15,27 +16,33 @@ using osu.Game.Users;
namespace osu.Game.Online.Multiplayer
{
[Serializable]
+ [MessagePackObject]
public class MultiplayerRoomUser : IEquatable
{
+ [Key(0)]
public readonly int UserID;
+ [Key(1)]
public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle;
///
/// The availability state of the current beatmap.
///
+ [Key(2)]
public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable();
///
/// Any mods applicable only to the local user.
///
+ [Key(3)]
[NotNull]
public IEnumerable UserMods { get; set; } = Enumerable.Empty();
+ [IgnoreMember]
public User? User { get; set; }
[JsonConstructor]
- public MultiplayerRoomUser(in int userId)
+ public MultiplayerRoomUser(int userId)
{
UserID = userId;
}
diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
index 597bee2764..69df2a69cc 100644
--- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
@@ -15,7 +15,6 @@ using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
-using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -105,7 +104,7 @@ namespace osu.Game.Online.Multiplayer
if (!connected.NewValue && Room != null)
{
Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important);
- LeaveRoom().CatchUnobservedExceptions();
+ LeaveRoom();
}
});
}
diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs
index e7dbc5f436..38bd236718 100644
--- a/osu.Game/Online/Rooms/BeatmapAvailability.cs
+++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using MessagePack;
using Newtonsoft.Json;
namespace osu.Game.Online.Rooms
@@ -9,20 +10,23 @@ namespace osu.Game.Online.Rooms
///
/// The local availability information about a certain beatmap for the client.
///
+ [MessagePackObject]
public class BeatmapAvailability : IEquatable
{
///
/// The beatmap's availability state.
///
+ [Key(0)]
public readonly DownloadState State;
///
/// The beatmap's downloading progress, null when not in state.
///
+ [Key(1)]
public readonly double? DownloadProgress;
[JsonConstructor]
- private BeatmapAvailability(DownloadState state, double? downloadProgress = null)
+ public BeatmapAvailability(DownloadState state, double? downloadProgress = null)
{
State = state;
DownloadProgress = downloadProgress;
diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs
index a8d0434324..0e59cdf4ce 100644
--- a/osu.Game/Online/Spectator/FrameDataBundle.cs
+++ b/osu.Game/Online/Spectator/FrameDataBundle.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
+using MessagePack;
using Newtonsoft.Json;
using osu.Game.Replays.Legacy;
using osu.Game.Scoring;
@@ -12,10 +13,13 @@ using osu.Game.Scoring;
namespace osu.Game.Online.Spectator
{
[Serializable]
+ [MessagePackObject]
public class FrameDataBundle
{
+ [Key(0)]
public FrameHeader Header { get; set; }
+ [Key(1)]
public IEnumerable Frames { get; set; }
public FrameDataBundle(ScoreInfo score, IEnumerable frames)
diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs
index 135b356eda..adfcbcd95a 100644
--- a/osu.Game/Online/Spectator/FrameHeader.cs
+++ b/osu.Game/Online/Spectator/FrameHeader.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
+using MessagePack;
using Newtonsoft.Json;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@@ -12,31 +13,37 @@ using osu.Game.Scoring;
namespace osu.Game.Online.Spectator
{
[Serializable]
+ [MessagePackObject]
public class FrameHeader
{
///
/// The current accuracy of the score.
///
+ [Key(0)]
public double Accuracy { get; set; }
///
/// The current combo of the score.
///
+ [Key(1)]
public int Combo { get; set; }
///
/// The maximum combo achieved up to the current point in time.
///
+ [Key(2)]
public int MaxCombo { get; set; }
///
/// Cumulative hit statistics.
///
+ [Key(3)]
public Dictionary Statistics { get; set; }
///
/// The time at which this frame was received by the server.
///
+ [Key(4)]
public DateTimeOffset ReceivedTime { get; set; }
///
@@ -54,7 +61,8 @@ namespace osu.Game.Online.Spectator
}
[JsonConstructor]
- public FrameHeader(int combo, int maxCombo, double accuracy, Dictionary statistics, DateTimeOffset receivedTime)
+ [SerializationConstructor]
+ public FrameHeader(double accuracy, int combo, int maxCombo, Dictionary statistics, DateTimeOffset receivedTime)
{
Combo = combo;
MaxCombo = maxCombo;
diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs
index 101ce3d5d5..96a875bc14 100644
--- a/osu.Game/Online/Spectator/SpectatorState.cs
+++ b/osu.Game/Online/Spectator/SpectatorState.cs
@@ -5,18 +5,23 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using MessagePack;
using osu.Game.Online.API;
namespace osu.Game.Online.Spectator
{
[Serializable]
+ [MessagePackObject]
public class SpectatorState : IEquatable
{
+ [Key(0)]
public int? BeatmapID { get; set; }
+ [Key(1)]
public int? RulesetID { get; set; }
[NotNull]
+ [Key(2)]
public IEnumerable Mods { get; set; } = Enumerable.Empty();
public bool Equals(SpectatorState other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods) && RulesetID == other?.RulesetID;
diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs
index 344b73f3d9..b95e3f1297 100644
--- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs
@@ -10,6 +10,7 @@ using JetBrains.Annotations;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
+using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -116,14 +117,19 @@ namespace osu.Game.Online.Spectator
if (connection != null)
return;
- connection = new HubConnectionBuilder()
- .WithUrl(endpoint, options =>
- {
- options.Headers.Add("Authorization", $"Bearer {api.AccessToken}");
- })
- .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; })
- .Build();
+ var builder = new HubConnectionBuilder()
+ .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); });
+ if (RuntimeInfo.SupportsJIT)
+ builder.AddMessagePackProtocol();
+ else
+ {
+ // eventually we will precompile resolvers for messagepack, but this isn't working currently
+ // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
+ builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
+ }
+
+ connection = builder.Build();
// until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198)
connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
index b429a5277b..01bcbd3244 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
@@ -12,7 +12,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using Humanizer;
-using osu.Game.Utils;
+using osu.Framework.Extensions.EnumExtensions;
namespace osu.Game.Overlays.BeatmapListing
{
@@ -80,7 +80,7 @@ namespace osu.Game.Overlays.BeatmapListing
if (typeof(T).IsEnum)
{
- foreach (var val in OrderAttributeUtils.GetValuesInOrder())
+ foreach (var val in EnumExtensions.GetValuesInOrder())
AddItem(val);
}
}
diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs
index eee5d8f7e1..015cee8ce3 100644
--- a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs
+++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs
@@ -1,7 +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.Game.Utils;
+using osu.Framework.Utils;
namespace osu.Game.Overlays.BeatmapListing
{
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 0c9c995dd6..698984b306 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -176,23 +176,34 @@ namespace osu.Game.Overlays
loadingLayer.Hide();
lastFetchDisplayedTime = Time.Current;
+ if (content == currentContent)
+ return;
+
var lastContent = currentContent;
if (lastContent != null)
{
- lastContent.FadeOut(100, Easing.OutQuint).Expire();
+ var transform = lastContent.FadeOut(100, Easing.OutQuint);
- // Consider the case when the new content is smaller than the last content.
- // If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird.
- // At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0.
- // To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so.
- lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y).Then().Schedule(() => panelTarget.Remove(lastContent));
+ if (lastContent == notFoundContent)
+ {
+ // not found display may be used multiple times, so don't expire/dispose it.
+ transform.Schedule(() => panelTarget.Remove(lastContent));
+ }
+ else
+ {
+ // Consider the case when the new content is smaller than the last content.
+ // If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird.
+ // At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0.
+ // To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so.
+ lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y).Then().Schedule(() => lastContent.Expire());
+ }
}
if (!content.IsAlive)
panelTarget.Add(content);
- content.FadeIn(200, Easing.OutQuint);
+ content.FadeInFromZero(200, Easing.OutQuint);
currentContent = content;
}
@@ -202,7 +213,7 @@ namespace osu.Game.Overlays
base.Dispose(isDisposing);
}
- private class NotFoundDrawable : CompositeDrawable
+ public class NotFoundDrawable : CompositeDrawable
{
public NotFoundDrawable()
{
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index 324299ccba..ddd1dfa6cd 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -15,7 +16,6 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Users.Drawables;
-using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@@ -105,7 +105,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
var ruleset = scores.First().Ruleset.CreateInstance();
- foreach (var result in OrderAttributeUtils.GetValuesInOrder())
+ foreach (var result in EnumExtensions.GetValuesInOrder())
{
if (!allScoreStatistics.Contains(result))
continue;
diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs
index 4eb348ae33..f43420e35e 100644
--- a/osu.Game/Overlays/Chat/ChatLine.cs
+++ b/osu.Game/Overlays/Chat/ChatLine.cs
@@ -190,13 +190,13 @@ namespace osu.Game.Overlays.Chat
}
}
};
-
- updateMessageContent();
}
protected override void LoadComplete()
{
base.LoadComplete();
+
+ updateMessageContent();
FinishTransforms(true);
}
diff --git a/osu.Game/Overlays/Comments/VotePill.cs b/osu.Game/Overlays/Comments/VotePill.cs
index aa9723ea85..cf3c470f96 100644
--- a/osu.Game/Overlays/Comments/VotePill.cs
+++ b/osu.Game/Overlays/Comments/VotePill.cs
@@ -33,11 +33,16 @@ namespace osu.Game.Overlays.Comments
[Resolved]
private IAPIProvider api { get; set; }
+ [Resolved(canBeNull: true)]
+ private LoginOverlay login { get; set; }
+
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
+ protected Box Background { get; private set; }
+
private readonly Comment comment;
- private Box background;
+
private Box hoverLayer;
private CircularContainer borderContainer;
private SpriteText sideNumber;
@@ -62,8 +67,12 @@ namespace osu.Game.Overlays.Comments
AccentColour = borderContainer.BorderColour = sideNumber.Colour = colours.GreenLight;
hoverLayer.Colour = Color4.Black.Opacity(0.5f);
- if (api.IsLoggedIn && api.LocalUser.Value.Id != comment.UserId)
+ var ownComment = api.LocalUser.Value.Id == comment.UserId;
+
+ if (!ownComment)
Action = onAction;
+
+ Background.Alpha = ownComment ? 0 : 1;
}
protected override void LoadComplete()
@@ -71,12 +80,18 @@ namespace osu.Game.Overlays.Comments
base.LoadComplete();
isVoted.Value = comment.IsVoted;
votesCount.Value = comment.VotesCount;
- isVoted.BindValueChanged(voted => background.Colour = voted.NewValue ? AccentColour : colourProvider.Background6, true);
+ isVoted.BindValueChanged(voted => Background.Colour = voted.NewValue ? AccentColour : colourProvider.Background6, true);
votesCount.BindValueChanged(count => votesCounter.Text = $"+{count.NewValue}", true);
}
private void onAction()
{
+ if (!api.IsLoggedIn)
+ {
+ login?.Show();
+ return;
+ }
+
request = new CommentVoteRequest(comment.Id, isVoted.Value ? CommentVoteAction.UnVote : CommentVoteAction.Vote);
request.Success += onSuccess;
api.Queue(request);
@@ -102,7 +117,7 @@ namespace osu.Game.Overlays.Comments
Masking = true,
Children = new Drawable[]
{
- background = new Box
+ Background = new Box
{
RelativeSizeAxes = Axes.Both
},
diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs
index ab8efdabcc..8e0d1f5bbd 100644
--- a/osu.Game/Overlays/Mods/ModButton.cs
+++ b/osu.Game/Overlays/Mods/ModButton.cs
@@ -236,13 +236,13 @@ namespace osu.Game.Overlays.Mods
{
iconsContainer.AddRange(new[]
{
- backgroundIcon = new PassThroughTooltipModIcon(Mods[1])
+ backgroundIcon = new ModIcon(Mods[1], false)
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
Position = new Vector2(1.5f),
},
- foregroundIcon = new PassThroughTooltipModIcon(Mods[0])
+ foregroundIcon = new ModIcon(Mods[0], false)
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
@@ -252,7 +252,7 @@ namespace osu.Game.Overlays.Mods
}
else
{
- iconsContainer.Add(foregroundIcon = new PassThroughTooltipModIcon(Mod)
+ iconsContainer.Add(foregroundIcon = new ModIcon(Mod, false)
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
@@ -297,15 +297,5 @@ namespace osu.Game.Overlays.Mods
Mod = mod;
}
-
- private class PassThroughTooltipModIcon : ModIcon
- {
- public override string TooltipText => null;
-
- public PassThroughTooltipModIcon(Mod mod)
- : base(mod)
- {
- }
- }
}
}
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 2950dc7489..25cb75f73a 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -259,9 +259,9 @@ namespace osu.Game.Overlays.Mods
},
new Drawable[]
{
- // Footer
new Container
{
+ Name = "Footer content",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Origin = Anchor.TopCentre,
@@ -280,10 +280,9 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.BottomCentre,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
+ RelativePositionAxes = Axes.X,
Width = content_width,
Spacing = new Vector2(footer_button_spacing, footer_button_spacing / 2),
- LayoutDuration = 100,
- LayoutEasing = Easing.OutQuint,
Padding = new MarginPadding
{
Vertical = 15,
@@ -385,7 +384,7 @@ namespace osu.Game.Overlays.Mods
{
base.PopOut();
- footerContainer.MoveToX(footerContainer.DrawSize.X, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
+ footerContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
footerContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
foreach (var section in ModSectionsContainer.Children)
diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs
index b67d5db1a4..0004719b87 100644
--- a/osu.Game/Overlays/OverlayScrollContainer.cs
+++ b/osu.Game/Overlays/OverlayScrollContainer.cs
@@ -17,9 +17,9 @@ using osuTK.Graphics;
namespace osu.Game.Overlays
{
///
- /// which provides . Mostly used in .
+ /// which provides . Mostly used in .
///
- public class OverlayScrollContainer : OsuScrollContainer
+ public class OverlayScrollContainer : UserTrackingScrollContainer
{
///
/// Scroll position at which the will be shown.
diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
index 658cdb8ce3..04a1040e06 100644
--- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
@@ -49,9 +49,12 @@ namespace osu.Game.Overlays.Profile.Header
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
- new AddFriendButton
+ new FollowersButton
+ {
+ User = { BindTarget = User }
+ },
+ new MappingSubscribersButton
{
- RelativeSizeAxes = Axes.Y,
User = { BindTarget = User }
},
new MessageUserButton
@@ -69,7 +72,6 @@ namespace osu.Game.Overlays.Profile.Header
Width = UserProfileOverlay.CONTENT_X_MARGIN,
Child = new ExpandDetailsButton
{
- RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
DetailsVisible = { BindTarget = DetailsVisible }
diff --git a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs b/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs
deleted file mode 100644
index 6c2b2dc16a..0000000000
--- a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs
+++ /dev/null
@@ -1,60 +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 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;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Users;
-using osuTK;
-
-namespace osu.Game.Overlays.Profile.Header.Components
-{
- public class AddFriendButton : ProfileHeaderButton
- {
- public readonly Bindable User = new Bindable();
-
- public override string TooltipText => "friends";
-
- private OsuSpriteText followerText;
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Child = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Direction = FillDirection.Horizontal,
- Padding = new MarginPadding { Right = 10 },
- Children = new Drawable[]
- {
- new SpriteIcon
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Icon = FontAwesome.Solid.User,
- FillMode = FillMode.Fit,
- Size = new Vector2(50, 14)
- },
- followerText = new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Font = OsuFont.GetFont(weight: FontWeight.Bold)
- }
- }
- };
-
- // todo: when friending/unfriending is implemented, the APIAccess.Friends list should be updated accordingly.
-
- User.BindValueChanged(user => updateFollowers(user.NewValue), true);
- }
-
- private void updateFollowers(User user) => followerText.Text = user?.FollowerCount.ToString("#,##0");
- }
-}
diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs
new file mode 100644
index 0000000000..bd8aa7b3bd
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs
@@ -0,0 +1,26 @@
+// 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.Sprites;
+using osu.Game.Users;
+
+namespace osu.Game.Overlays.Profile.Header.Components
+{
+ public class FollowersButton : ProfileHeaderStatisticsButton
+ {
+ public readonly Bindable User = new Bindable();
+
+ public override string TooltipText => "followers";
+
+ protected override IconUsage Icon => FontAwesome.Solid.User;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ // todo: when friending/unfriending is implemented, the APIAccess.Friends list should be updated accordingly.
+ User.BindValueChanged(user => SetValue(user.NewValue?.FollowerCount ?? 0), true);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs
new file mode 100644
index 0000000000..b4d7c9a05c
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs
@@ -0,0 +1,25 @@
+// 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.Sprites;
+using osu.Game.Users;
+
+namespace osu.Game.Overlays.Profile.Header.Components
+{
+ public class MappingSubscribersButton : ProfileHeaderStatisticsButton
+ {
+ public readonly Bindable User = new Bindable();
+
+ public override string TooltipText => "mapping subscribers";
+
+ protected override IconUsage Icon => FontAwesome.Solid.Bell;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ User.BindValueChanged(user => SetValue(user.NewValue?.MappingFollowerCount ?? 0), true);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs
index cc6edcdd6a..228765ee1a 100644
--- a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs
@@ -33,7 +33,6 @@ namespace osu.Game.Overlays.Profile.Header.Components
public MessageUserButton()
{
Content.Alpha = 0;
- RelativeSizeAxes = Axes.Y;
Child = new SpriteIcon
{
diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs
index e14d73dd98..cea63574cf 100644
--- a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs
@@ -22,6 +22,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
protected ProfileHeaderButton()
{
AutoSizeAxes = Axes.X;
+ Height = 40;
base.Content.Add(new CircularContainer
{
diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs
new file mode 100644
index 0000000000..b65d5e2329
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs
@@ -0,0 +1,51 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+
+namespace osu.Game.Overlays.Profile.Header.Components
+{
+ public abstract class ProfileHeaderStatisticsButton : ProfileHeaderButton
+ {
+ private readonly OsuSpriteText drawableText;
+
+ protected ProfileHeaderStatisticsButton()
+ {
+ Child = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.X,
+ RelativeSizeAxes = Axes.Y,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ new SpriteIcon
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Icon = Icon,
+ FillMode = FillMode.Fit,
+ Size = new Vector2(50, 14)
+ },
+ drawableText = new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Margin = new MarginPadding { Right = 10 },
+ Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold)
+ }
+ }
+ };
+ }
+
+ protected abstract IconUsage Icon { get; }
+
+ protected void SetValue(int value) => drawableText.Text = value.ToString("#,##0");
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index 4cfd801caf..7c8309fd56 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -108,7 +108,7 @@ namespace osu.Game.Overlays.Settings.Sections
if (skinDropdown.Items.All(s => s.ID != configBindable.Value))
configBindable.Value = 0;
- configBindable.BindValueChanged(id => dropdownBindable.Value = skinDropdown.Items.Single(s => s.ID == id.NewValue), true);
+ configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig), true);
dropdownBindable.BindValueChanged(skin =>
{
if (skin.NewValue == random_skin_info)
@@ -121,6 +121,23 @@ namespace osu.Game.Overlays.Settings.Sections
});
}
+ private void updateSelectedSkinFromConfig()
+ {
+ int id = configBindable.Value;
+
+ var skin = skinDropdown.Items.FirstOrDefault(s => s.ID == id);
+
+ if (skin == null)
+ {
+ // there may be a thread race condition where an item is selected that hasn't yet been added to the dropdown.
+ // to avoid adding complexity, let's just ensure the item is added so we can perform the selection.
+ skin = skins.Query(s => s.ID == id);
+ addItem(skin);
+ }
+
+ dropdownBindable.Value = skin;
+ }
+
private void updateItems()
{
skinItems = skins.GetAllUsableSkins();
@@ -132,14 +149,14 @@ namespace osu.Game.Overlays.Settings.Sections
private void itemUpdated(ValueChangedEvent> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
- {
- Schedule(() =>
- {
- List newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList();
- sortUserSkins(newDropdownItems);
- skinDropdown.Items = newDropdownItems;
- });
- }
+ Schedule(() => addItem(item));
+ }
+
+ private void addItem(SkinInfo item)
+ {
+ List newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList();
+ sortUserSkins(newDropdownItems);
+ skinDropdown.Items = newDropdownItems;
}
private void itemRemoved(ValueChangedEvent> weakItem)
diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index 81027667fa..7f29545c2e 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -202,7 +202,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both;
}
- protected override OsuScrollContainer CreateScrollContainer() => new OverlayScrollContainer();
+ protected override UserTrackingScrollContainer CreateScrollContainer() => new OverlayScrollContainer();
protected override FlowContainer CreateScrollContentContainer() => new FillFlowContainer
{
diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs
index 74bacae9e1..ab9ccda9b9 100644
--- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs
+++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs
@@ -1,38 +1,51 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using MessagePack;
using Newtonsoft.Json;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Replays.Legacy
{
+ [MessagePackObject]
public class LegacyReplayFrame : ReplayFrame
{
[JsonIgnore]
+ [IgnoreMember]
public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0);
+ [Key(1)]
public float? MouseX;
+
+ [Key(2)]
public float? MouseY;
[JsonIgnore]
+ [IgnoreMember]
public bool MouseLeft => MouseLeft1 || MouseLeft2;
[JsonIgnore]
+ [IgnoreMember]
public bool MouseRight => MouseRight1 || MouseRight2;
[JsonIgnore]
+ [IgnoreMember]
public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1);
[JsonIgnore]
+ [IgnoreMember]
public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1);
[JsonIgnore]
+ [IgnoreMember]
public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2);
[JsonIgnore]
+ [IgnoreMember]
public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2);
+ [Key(3)]
public ReplayButtonState ButtonState;
public LegacyReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState)
diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs
index 0e589735c1..4edcb0b074 100644
--- a/osu.Game/Rulesets/Mods/ModHardRock.cs
+++ b/osu.Game/Rulesets/Mods/ModHardRock.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods
{
}
- public void ApplyToDifficulty(BeatmapDifficulty difficulty)
+ public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
const float ratio = 1.4f;
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.
diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
index 4d43ae73d3..b6916c838e 100644
--- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs
+++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mods
///
/// The point in the beatmap at which the final ramping rate should be reached.
///
- private const double final_rate_progress = 0.75f;
+ public const double FINAL_RATE_PROGRESS = 0.75f;
[SettingSource("Initial rate", "The starting speed of the track")]
public abstract BindableNumber InitialRate { get; }
@@ -66,17 +66,18 @@ namespace osu.Game.Rulesets.Mods
public virtual void ApplyToBeatmap(IBeatmap beatmap)
{
- HitObject lastObject = beatmap.HitObjects.LastOrDefault();
-
SpeedChange.SetDefault();
- beginRampTime = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0;
- finalRateTime = final_rate_progress * (lastObject?.GetEndTime() ?? 0);
+ double firstObjectStart = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0;
+ double lastObjectEnd = beatmap.HitObjects.LastOrDefault()?.GetEndTime() ?? 0;
+
+ beginRampTime = firstObjectStart;
+ finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart);
}
public virtual void Update(Playfield playfield)
{
- applyRateAdjustment((track.CurrentTime - beginRampTime) / finalRateTime);
+ applyRateAdjustment((track.CurrentTime - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime));
}
///
diff --git a/osu.Game/Rulesets/Replays/ReplayFrame.cs b/osu.Game/Rulesets/Replays/ReplayFrame.cs
index 85e068ae79..7de53211a2 100644
--- a/osu.Game/Rulesets/Replays/ReplayFrame.cs
+++ b/osu.Game/Rulesets/Replays/ReplayFrame.cs
@@ -1,10 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using MessagePack;
+
namespace osu.Game.Rulesets.Replays
{
+ [MessagePackObject]
public class ReplayFrame
{
+ [Key(0)]
public double Time;
public ReplayFrame()
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index b3b3d11ab3..dbc2bd4d01 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -24,9 +24,9 @@ using osu.Game.Skinning;
using osu.Game.Users;
using JetBrains.Annotations;
using osu.Framework.Extensions;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Testing;
using osu.Game.Screens.Ranking.Statistics;
-using osu.Game.Utils;
namespace osu.Game.Rulesets
{
@@ -272,7 +272,7 @@ namespace osu.Game.Rulesets
var validResults = GetValidHitResults();
// enumerate over ordered list to guarantee return order is stable.
- foreach (var result in OrderAttributeUtils.GetValuesInOrder())
+ foreach (var result in EnumExtensions.GetValuesInOrder())
{
switch (result)
{
@@ -298,7 +298,7 @@ namespace osu.Game.Rulesets
///
/// is implicitly included. Special types like are ignored even when specified.
///
- protected virtual IEnumerable GetValidHitResults() => OrderAttributeUtils.GetValuesInOrder();
+ protected virtual IEnumerable GetValidHitResults() => EnumExtensions.GetValuesInOrder();
///
/// Get a display friendly name for the specified result type.
diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs
index 6a3a034fc1..eaa1f95744 100644
--- a/osu.Game/Rulesets/Scoring/HitResult.cs
+++ b/osu.Game/Rulesets/Scoring/HitResult.cs
@@ -3,7 +3,7 @@
using System.ComponentModel;
using System.Diagnostics;
-using osu.Game.Utils;
+using osu.Framework.Utils;
namespace osu.Game.Rulesets.Scoring
{
diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs
index 76fcb8e080..981fe7f4a6 100644
--- a/osu.Game/Rulesets/UI/ModIcon.cs
+++ b/osu.Game/Rulesets/UI/ModIcon.cs
@@ -16,6 +16,9 @@ using osu.Framework.Bindables;
namespace osu.Game.Rulesets.UI
{
+ ///
+ /// Display the specified mod at a fixed size.
+ ///
public class ModIcon : Container, IHasTooltip
{
public readonly BindableBool Selected = new BindableBool();
@@ -26,9 +29,10 @@ namespace osu.Game.Rulesets.UI
private const float size = 80;
- public virtual string TooltipText => mod.IconTooltip;
+ public virtual string TooltipText => showTooltip ? mod.IconTooltip : null;
private Mod mod;
+ private readonly bool showTooltip;
public Mod Mod
{
@@ -48,9 +52,15 @@ namespace osu.Game.Rulesets.UI
private Color4 backgroundColour;
private Color4 highlightedColour;
- public ModIcon(Mod mod)
+ ///
+ /// Construct a new instance.
+ ///
+ /// The mod to be displayed
+ /// Whether a tooltip describing the mod should display on hover.
+ public ModIcon(Mod mod, bool showTooltip = true)
{
this.mod = mod ?? throw new ArgumentNullException(nameof(mod));
+ this.showTooltip = showTooltip;
Size = new Vector2(size);
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs
index 103e39e78a..8298cf4773 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
@@ -13,7 +12,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
///
public class BookmarkPart : TimelinePart
{
- protected override void LoadBeatmap(WorkingBeatmap beatmap)
+ protected override void LoadBeatmap(EditorBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
foreach (int bookmark in beatmap.BeatmapInfo.Bookmarks)
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs
index ceccbffc9c..e8a4b5c8c7 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
@@ -14,10 +13,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
///
public class BreakPart : TimelinePart
{
- protected override void LoadBeatmap(WorkingBeatmap beatmap)
+ protected override void LoadBeatmap(EditorBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
- foreach (var breakPeriod in beatmap.Beatmap.Breaks)
+ foreach (var breakPeriod in beatmap.Breaks)
Add(new BreakVisualisation(breakPeriod));
}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs
index e76ab71e54..70afc1e308 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs
@@ -4,7 +4,6 @@
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Bindables;
-using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
@@ -16,12 +15,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
private readonly IBindableList controlPointGroups = new BindableList();
- protected override void LoadBeatmap(WorkingBeatmap beatmap)
+ protected override void LoadBeatmap(EditorBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
controlPointGroups.UnbindAll();
- controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups);
+ controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((sender, args) =>
{
switch (args.Action)
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs
index 5a2214509c..d551333616 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs
@@ -2,15 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
-using osu.Game.Beatmaps;
using osu.Game.Graphics;
+using osuTK;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
@@ -54,9 +53,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
scheduledSeek?.Cancel();
scheduledSeek = Schedule(() =>
{
- if (Beatmap.Value == null)
- return;
-
float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth);
editorClock.SeekSmoothlyTo(markerPos / DrawWidth * editorClock.TrackLength);
});
@@ -68,7 +64,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
marker.X = (float)editorClock.CurrentTime;
}
- protected override void LoadBeatmap(WorkingBeatmap beatmap)
+ protected override void LoadBeatmap(EditorBeatmap beatmap)
{
// block base call so we don't clear our marker (can be reused on beatmap change).
}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs
index 5b8f7c747b..5aba81aa7d 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs
@@ -21,7 +21,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
///
public class TimelinePart : Container where T : Drawable
{
- protected readonly IBindable Beatmap = new Bindable();
+ private readonly IBindable beatmap = new Bindable();
+
+ [Resolved]
+ protected EditorBeatmap EditorBeatmap { get; private set; }
protected readonly IBindable
-
+
+
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 4732620085..48dc01f5de 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -88,7 +88,7 @@
-
+