diff --git a/FodyWeavers.xml b/FodyWeavers.xml index cc07b89533..ea490e3297 100644 --- a/FodyWeavers.xml +++ b/FodyWeavers.xml @@ -1,3 +1,3 @@  - + \ No newline at end of file diff --git a/osu.Android.props b/osu.Android.props index 3c4380e355..481ddc118f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 1c89d9cd00..f89750a96e 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mania.Configuration; @@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Mania private class TimeSlider : OsuSliderBar { - public override string TooltipText => Current.Value.ToString("N0") + "ms"; + public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms"; } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index e111bb1054..3252e6d912 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -37,11 +37,13 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly BindableBool snakingIn = new BindableBool(); private readonly BindableBool snakingOut = new BindableBool(); + private IBeatmap beatmap; + private const double duration_of_span = 3605; private const double fade_in_modifier = -1200; protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + => new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); [BackgroundDependencyLoader] private void load(RulesetConfigCache configCache) @@ -51,8 +53,16 @@ namespace osu.Game.Rulesets.Osu.Tests config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); } + private Slider slider; private DrawableSlider drawableSlider; + [SetUp] + public void Setup() => Schedule(() => + { + slider = null; + drawableSlider = null; + }); + [SetUpSteps] public override void SetUpSteps() { @@ -67,21 +77,19 @@ namespace osu.Game.Rulesets.Osu.Tests base.SetUpSteps(); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); - double startTime = hitObjects[sliderIndex].StartTime; - addSeekStep(startTime); - retrieveDrawableSlider((Slider)hitObjects[sliderIndex]); + retrieveSlider(sliderIndex); setSnaking(true); - ensureSnakingIn(startTime + fade_in_modifier); + addEnsureSnakingInSteps(() => slider.StartTime + fade_in_modifier); for (int i = 0; i < sliderIndex; i++) { // non-final repeats should not snake out - ensureNoSnakingOut(startTime, i); + addEnsureNoSnakingOutStep(() => slider.StartTime, i); } // final repeat should snake out - ensureSnakingOut(startTime, sliderIndex); + addEnsureSnakingOutSteps(() => slider.StartTime, sliderIndex); } [TestCase(0)] @@ -93,17 +101,15 @@ namespace osu.Game.Rulesets.Osu.Tests base.SetUpSteps(); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); - double startTime = hitObjects[sliderIndex].StartTime; - addSeekStep(startTime); - retrieveDrawableSlider((Slider)hitObjects[sliderIndex]); + retrieveSlider(sliderIndex); setSnaking(false); - ensureNoSnakingIn(startTime + fade_in_modifier); + addEnsureNoSnakingInSteps(() => slider.StartTime + fade_in_modifier); for (int i = 0; i <= sliderIndex; i++) { // no snaking out ever, including final repeat - ensureNoSnakingOut(startTime, i); + addEnsureNoSnakingOutStep(() => slider.StartTime, i); } } @@ -116,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests // repeat might have a chance to update its position depending on where in the frame its hit, // so some leniency is allowed here instead of checking strict equality - checkPositionChange(16600, sliderRepeat, positionAlmostSame); + addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame); } [Test] @@ -126,38 +132,41 @@ namespace osu.Game.Rulesets.Osu.Tests setSnaking(true); base.SetUpSteps(); - checkPositionChange(16600, sliderRepeat, positionDecreased); + addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased); } - private void retrieveDrawableSlider(Slider slider) => AddUntilStep($"retrieve slider @ {slider.StartTime}", () => - (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); - - private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased); - private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame); - - private void ensureSnakingOut(double startTime, int repeatIndex) + private void retrieveSlider(int index) { - var repeatTime = timeAtRepeat(startTime, repeatIndex); + AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]); + addSeekStep(() => slider); + AddUntilStep("retrieve drawable slider", () => + (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); + } + private void addEnsureSnakingInSteps(Func startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased); + private void addEnsureNoSnakingInSteps(Func startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionRemainsSame); + + private void addEnsureSnakingOutSteps(Func startTime, int repeatIndex) + { if (repeatIndex % 2 == 0) - checkPositionChange(repeatTime, sliderStart, positionIncreased); + addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderStart, positionIncreased); else - checkPositionChange(repeatTime, sliderEnd, positionDecreased); + addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderEnd, positionDecreased); } - private void ensureNoSnakingOut(double startTime, int repeatIndex) => - checkPositionChange(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame); + private void addEnsureNoSnakingOutStep(Func startTime, int repeatIndex) + => addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame); - private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex; - private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)sliderStart : sliderEnd; + private Func timeAtRepeat(Func startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex; + private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)getSliderStart : getSliderEnd; - private List sliderCurve => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; - private Vector2 sliderStart() => sliderCurve.First(); - private Vector2 sliderEnd() => sliderCurve.Last(); + private List getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; + private Vector2 getSliderStart() => getSliderCurve().First(); + private Vector2 getSliderEnd() => getSliderCurve().Last(); - private Vector2 sliderRepeat() + private Vector2 getSliderRepeat() { - var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObjects[1]); + var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == beatmap.HitObjects[1]); var repeat = drawable.ChildrenOfType>().First().Children.First(); return repeat.Position; } @@ -167,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Tests private bool positionDecreased(Vector2 previous, Vector2 current) => current.X < previous.X && current.Y < previous.Y; private bool positionAlmostSame(Vector2 previous, Vector2 current) => Precision.AlmostEquals(previous, current, 1); - private void checkPositionChange(double startTime, Func positionToCheck, Func positionAssertion) + private void addCheckPositionChangeSteps(Func startTime, Func positionToCheck, Func positionAssertion) { Vector2 previousPosition = Vector2.Zero; @@ -176,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(startTime); AddStep($"save {positionDescription} position", () => previousPosition = positionToCheck.Invoke()); - addSeekStep(startTime + 100); + addSeekStep(() => startTime() + 100); AddAssert($"{positionDescription} {assertionDescription}", () => { var currentPosition = positionToCheck.Invoke(); @@ -193,19 +202,21 @@ namespace osu.Game.Rulesets.Osu.Tests }); } - private void addSeekStep(double time) + private void addSeekStep(Func slider) { - AddStep($"seek to {time}", () => MusicController.SeekTo(time)); - - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); + AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime)); + AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap + private void addSeekStep(Func time) { - HitObjects = hitObjects - }; + AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time())); + AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); + } - private readonly List hitObjects = new List + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() }; + + private static List createHitObjects() => new List { new Slider { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 8ff21057b5..9da583a073 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void addSeekStep(double time) { - AddStep($"seek to {time}", () => MusicController.SeekTo(time)); + AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 48e4db11ca..5b476526c9 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; @@ -283,6 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } - public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty; + public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty; } } diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 0d117f8755..5dc25d6643 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -19,7 +19,9 @@ using osu.Game.Database; using osu.Game.IO; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Scoring; using osu.Game.Tests.Resources; +using osu.Game.Tests.Scores.IO; using osu.Game.Users; using SharpCompress.Archives; using SharpCompress.Archives.Zip; @@ -185,13 +187,62 @@ namespace osu.Game.Tests.Beatmaps.IO } } - private string hashFile(string filename) + [Test] + public async Task TestImportThenImportWithChangedHashedFile() { - using (var s = File.OpenRead(filename)) - return s.ComputeMD5Hash(); + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) + { + try + { + var osu = LoadOsuIntoHost(host); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + var imported = await LoadOszIntoOsu(osu); + + await createScoreForBeatmap(osu, imported.Beatmaps.First()); + + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + // arbitrary write to hashed file + // this triggers the special BeatmapManager.PreImport deletion/replacement flow. + using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText()) + await sw.WriteLineAsync("// changed"); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + var importedSecondTime = await osu.Dependencies.Get().Import(new ImportTask(temp)); + + ensureLoaded(osu); + + // check the newly "imported" beatmap is not the original. + Assert.IsTrue(imported.ID != importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + } + finally + { + Directory.Delete(extractedFolder, true); + } + } + finally + { + host.Exit(); + } + } } [Test] + [Ignore("intentionally broken by import optimisations")] public async Task TestImportThenImportWithChangedFile() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) @@ -294,6 +345,7 @@ namespace osu.Game.Tests.Beatmaps.IO } [Test] + [Ignore("intentionally broken by import optimisations")] public async Task TestImportCorruptThenImport() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. @@ -439,12 +491,11 @@ namespace osu.Game.Tests.Beatmaps.IO } } - [TestCase(true)] - [TestCase(false)] - public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) + [Test] + public async Task TestImportThenDeleteThenImportWithOnlineIDsMissing() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}")) { try { @@ -452,10 +503,8 @@ namespace osu.Game.Tests.Beatmaps.IO var imported = await LoadOszIntoOsu(osu); - if (set) - imported.OnlineBeatmapSetID = 1234; - else - imported.Beatmaps.First().OnlineBeatmapID = 1234; + foreach (var b in imported.Beatmaps) + b.OnlineBeatmapID = null; osu.Dependencies.Get().Update(imported); @@ -895,7 +944,17 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); } - private void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false) + private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap) + { + return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo + { + OnlineScoreID = 2, + Beatmap = beatmap, + BeatmapInfoID = beatmap.ID + }, new ImportScoreTest.TestArchiveReader()); + } + + private static void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false) { var manager = osu.Dependencies.Get(); @@ -904,12 +963,18 @@ namespace osu.Game.Tests.Beatmaps.IO : manager.GetAllUsableBeatmapSets().Count); } - private void checkBeatmapCount(OsuGameBase osu, int expected) + private static string hashFile(string filename) + { + using (var s = File.OpenRead(filename)) + return s.ComputeMD5Hash(); + } + + private static void checkBeatmapCount(OsuGameBase osu, int expected) { Assert.AreEqual(expected, osu.Dependencies.Get().QueryBeatmaps(_ => true).ToList().Count); } - private void checkSingleReferencedFileCount(OsuGameBase osu, int expected) + private static void checkSingleReferencedFileCount(OsuGameBase osu, int expected) { Assert.AreEqual(expected, osu.Dependencies.Get().QueryFiles(f => f.ReferenceCount == 1).Count()); } diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs new file mode 100644 index 0000000000..0ec21a4c7b --- /dev/null +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -0,0 +1,105 @@ +// 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.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Chat; +using osu.Game.Tests.Visual; +using osu.Game.Users; + +namespace osu.Game.Tests.Chat +{ + [HeadlessTest] + public class TestSceneChannelManager : OsuTestScene + { + private ChannelManager channelManager; + private int currentMessageId; + + [SetUp] + public void Setup() => Schedule(() => + { + var container = new ChannelManagerContainer(); + Child = container; + channelManager = container.ChannelManager; + }); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("register request handling", () => + { + currentMessageId = 0; + + ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case JoinChannelRequest joinChannel: + joinChannel.TriggerSuccess(); + return true; + + case PostMessageRequest postMessage: + postMessage.TriggerSuccess(new Message(++currentMessageId) + { + IsAction = postMessage.Message.IsAction, + ChannelId = postMessage.Message.ChannelId, + Content = postMessage.Message.Content, + Links = postMessage.Message.Links, + Timestamp = postMessage.Message.Timestamp, + Sender = postMessage.Message.Sender + }); + + return true; + } + + return false; + }; + }); + } + + [Test] + public void TestCommandsPostedToCorrectChannelWhenNotCurrent() + { + Channel channel1 = null; + Channel channel2 = null; + + AddStep("join 2 rooms", () => + { + channelManager.JoinChannel(channel1 = createChannel(1, ChannelType.Public)); + channelManager.JoinChannel(channel2 = createChannel(2, ChannelType.Public)); + }); + + AddStep("select channel 1", () => channelManager.CurrentChannel.Value = channel1); + + AddStep("post /me command to channel 2", () => channelManager.PostCommand("me dances", channel2)); + AddAssert("/me command received by channel 2", () => channel2.Messages.Last().Content == "dances"); + + AddStep("post /np command to channel 2", () => channelManager.PostCommand("np", channel2)); + AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to")); + } + + private Channel createChannel(int id, ChannelType type) => new Channel(new User()) + { + Id = id, + Name = $"Channel {id}", + Topic = $"Topic of channel {id} with type {type}", + Type = type, + }; + + private class ChannelManagerContainer : CompositeDrawable + { + [Cached] + public ChannelManager ChannelManager { get; } = new ChannelManager(); + + public ChannelManagerContainer() + { + InternalChild = ChannelManager; + } + } + } +} diff --git a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs new file mode 100644 index 0000000000..cf5b3a42a4 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs @@ -0,0 +1,241 @@ +// 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.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckFewHitsoundsTest + { + private CheckFewHitsounds check; + + private List notHitsounded; + private List hitsounded; + + [SetUp] + public void Setup() + { + check = new CheckFewHitsounds(); + notHitsounded = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; + hitsounded = new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL), + new HitSampleInfo(HitSampleInfo.HIT_FINISH) + }; + } + + [Test] + public void TestHitsounded() + { + var hitObjects = new List(); + + for (int i = 0; i < 16; ++i) + { + var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; + + if ((i + 1) % 2 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP)); + if ((i + 1) % 3 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE)); + if ((i + 1) % 4 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); + + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); + } + + assertOk(hitObjects); + } + + [Test] + public void TestHitsoundedWithBreak() + { + var hitObjects = new List(); + + for (int i = 0; i < 32; ++i) + { + var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; + + if ((i + 1) % 2 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP)); + if ((i + 1) % 3 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE)); + if ((i + 1) % 4 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); + // Leaves a gap in which no hitsounds exist or can be added, and so shouldn't be an issue. + if (i > 8 && i < 24) + continue; + + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); + } + + assertOk(hitObjects); + } + + [Test] + public void TestLightlyHitsounded() + { + var hitObjects = new List(); + + for (int i = 0; i < 30; ++i) + { + var samples = i % 8 == 0 ? hitsounded : notHitsounded; + + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); + } + + assertLongPeriodNegligible(hitObjects, count: 3); + } + + [Test] + public void TestRarelyHitsounded() + { + var hitObjects = new List(); + + for (int i = 0; i < 30; ++i) + { + var samples = (i == 0 || i == 15) ? hitsounded : notHitsounded; + + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); + } + + // Should prompt one warning between 1st and 16th, and another between 16th and 31st. + assertLongPeriodWarning(hitObjects, count: 2); + } + + [Test] + public void TestExtremelyRarelyHitsounded() + { + var hitObjects = new List(); + + for (int i = 0; i < 80; ++i) + { + var samples = i == 40 ? hitsounded : notHitsounded; + + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); + } + + // Should prompt one problem between 1st and 41st, and another between 41st and 81st. + assertLongPeriodProblem(hitObjects, count: 2); + } + + [Test] + public void TestNotHitsounded() + { + var hitObjects = new List(); + + for (int i = 0; i < 20; ++i) + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = notHitsounded }); + + assertNoHitsounds(hitObjects); + } + + [Test] + public void TestNestedObjectsHitsounded() + { + var ticks = new List(); + for (int i = 1; i < 16; ++i) + ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = hitsounded }); + + var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000) + { + Samples = hitsounded + }; + nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + assertOk(new List { nested }); + } + + [Test] + public void TestNestedObjectsRarelyHitsounded() + { + var ticks = new List(); + for (int i = 1; i < 16; ++i) + ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = i == 0 ? hitsounded : notHitsounded }); + + var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000) + { + Samples = hitsounded + }; + nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + assertLongPeriodWarning(new List { nested }); + } + + [Test] + public void TestConcurrentObjects() + { + var hitObjects = new List(); + + var ticks = new List(); + for (int i = 1; i < 10; ++i) + ticks.Add(new SliderTick { StartTime = 5000 * i, Samples = hitsounded }); + + var nested = new MockNestableHitObject(ticks.ToList(), 0, 50000) + { + Samples = notHitsounded + }; + nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + hitObjects.Add(nested); + + for (int i = 1; i <= 6; ++i) + hitObjects.Add(new HitCircle { StartTime = 10000 * i, Samples = notHitsounded }); + + assertOk(hitObjects); + } + + private void assertOk(List hitObjects) + { + Assert.That(check.Run(getContext(hitObjects)), Is.Empty); + } + + private void assertLongPeriodProblem(List hitObjects, int count = 1) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodProblem)); + } + + private void assertLongPeriodWarning(List hitObjects, int count = 1) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodWarning)); + } + + private void assertLongPeriodNegligible(List hitObjects, int count = 1) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodNegligible)); + } + + private void assertNoHitsounds(List hitObjects) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds)); + } + + private BeatmapVerifierContext getContext(List hitObjects) + { + var beatmap = new Beatmap { HitObjects = hitObjects }; + + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs new file mode 100644 index 0000000000..41a8f72305 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs @@ -0,0 +1,289 @@ +// 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.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckMutedObjectsTest + { + private CheckMutedObjects check; + private ControlPointInfo cpi; + + private const int volume_regular = 50; + private const int volume_low = 15; + private const int volume_muted = 5; + + [SetUp] + public void Setup() + { + check = new CheckMutedObjects(); + + cpi = new ControlPointInfo(); + cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular }); + cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low }); + cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted }); + } + + [Test] + public void TestNormalControlPointVolume() + { + var hitcircle = new HitCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertOk(new List { hitcircle }); + } + + [Test] + public void TestLowControlPointVolume() + { + var hitcircle = new HitCircle + { + StartTime = 1000, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertLowVolume(new List { hitcircle }); + } + + [Test] + public void TestMutedControlPointVolume() + { + var hitcircle = new HitCircle + { + StartTime = 2000, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMuted(new List { hitcircle }); + } + + [Test] + public void TestNormalSampleVolume() + { + // The sample volume should take precedence over the control point volume. + var hitcircle = new HitCircle + { + StartTime = 2000, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertOk(new List { hitcircle }); + } + + [Test] + public void TestLowSampleVolume() + { + var hitcircle = new HitCircle + { + StartTime = 2000, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_low) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertLowVolume(new List { hitcircle }); + } + + [Test] + public void TestMutedSampleVolume() + { + var hitcircle = new HitCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMuted(new List { hitcircle }); + } + + [Test] + public void TestNormalSampleVolumeSlider() + { + var sliderHead = new SliderHeadCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var sliderTick = new SliderTick + { + StartTime = 250, + Samples = new List { new HitSampleInfo("slidertick", volume: volume_muted) } // Should be fine. + }; + sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500) + { + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + slider.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertOk(new List { slider }); + } + + [Test] + public void TestMutedSampleVolumeSliderHead() + { + var sliderHead = new SliderHeadCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } + }; + sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var sliderTick = new SliderTick + { + StartTime = 250, + Samples = new List { new HitSampleInfo("slidertick") } + }; + sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500) + { + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail. + }; + slider.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMuted(new List { slider }); + } + + [Test] + public void TestMutedSampleVolumeSliderTail() + { + var sliderHead = new SliderHeadCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var sliderTick = new SliderTick + { + StartTime = 250, + Samples = new List { new HitSampleInfo("slidertick") } + }; + sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500) + { + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } // Applies to the tail. + }; + slider.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMutedPassive(new List { slider }); + } + + [Test] + public void TestMutedControlPointVolumeSliderHead() + { + var sliderHead = new SliderHeadCircle + { + StartTime = 2000, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var sliderTick = new SliderTick + { + StartTime = 2250, + Samples = new List { new HitSampleInfo("slidertick") } + }; + sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500) + { + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } + }; + slider.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMuted(new List { slider }); + } + + [Test] + public void TestMutedControlPointVolumeSliderTail() + { + var sliderHead = new SliderHeadCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var sliderTick = new SliderTick + { + StartTime = 250, + Samples = new List { new HitSampleInfo("slidertick") } + }; + sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); + + // Ends after the 5% control point. + var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500) + { + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + slider.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMutedPassive(new List { slider }); + } + + private void assertOk(List hitObjects) + { + Assert.That(check.Run(getContext(hitObjects)), Is.Empty); + } + + private void assertLowVolume(List hitObjects, int count = 1) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateLowVolumeActive)); + } + + private void assertMuted(List hitObjects, int count = 1) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedActive)); + } + + private void assertMutedPassive(List hitObjects) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Any(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedPassive)); + } + + private BeatmapVerifierContext getContext(List hitObjects) + { + var beatmap = new Beatmap + { + ControlPointInfo = cpi, + HitObjects = hitObjects + }; + + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs b/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs new file mode 100644 index 0000000000..29938839d3 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs @@ -0,0 +1,36 @@ +// 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.Threading; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Tests.Editing.Checks +{ + public sealed class MockNestableHitObject : HitObject, IHasDuration + { + private readonly IEnumerable toBeNested; + + public MockNestableHitObject(IEnumerable toBeNested, double startTime, double endTime) + { + this.toBeNested = toBeNested; + StartTime = startTime; + EndTime = endTime; + } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + foreach (var hitObject in toBeNested) + AddNested(hitObject); + } + + public double EndTime { get; } + + public double Duration + { + get => EndTime - StartTime; + set => throw new System.NotImplementedException(); + } + } +} diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index ea351e0d45..e888f51e98 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -17,7 +17,8 @@ namespace osu.Game.Tests protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false) { var osu = new TestOsuGameBase(withBeatmap); - Task.Run(() => host.Run(osu)); + Task.Run(() => host.Run(osu)) + .ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted); waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 9bd262a569..a55bdd2df8 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -90,6 +90,20 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.Less(filterCriteria.DrainRate.Min, 6.1f); } + [Test] + public void TestApplyOverallDifficultyQueries() + { + const string query = "od>4 easy od<8"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("easy", filterCriteria.SearchText.Trim()); + Assert.AreEqual(1, filterCriteria.SearchTerms.Length); + Assert.Greater(filterCriteria.OverallDifficulty.Min, 4.0); + Assert.Less(filterCriteria.OverallDifficulty.Min, 4.1); + Assert.Greater(filterCriteria.OverallDifficulty.Max, 7.9); + Assert.Less(filterCriteria.OverallDifficulty.Max, 8.0); + } + [Test] public void TestApplyBPMQueries() { diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index adc1d6aede..0983b806e2 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Users; @@ -50,7 +51,10 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddStep("create room initially in gameplay", () => { - Room.RoomID.Value = null; + var newRoom = new Room(); + newRoom.CopyFrom(SelectedRoom.Value); + + newRoom.RoomID.Value = null; Client.RoomSetupAction = room => { room.State = MultiplayerRoomState.Playing; @@ -61,7 +65,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer }); }; - RoomManager.CreateRoom(Room); + RoomManager.CreateRoom(newRoom); }); AddUntilStep("wait for room join", () => Client.Room != null); diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index d4e591cf09..6851df3832 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -31,32 +31,24 @@ namespace osu.Game.Tests.OnlinePlay } [Test] - public void TestMasterClockStartsWhenAllPlayerClocksHaveFrames() + public void TestPlayerClocksStartWhenAllHaveFrames() { setWaiting(() => player1, false); - assertMasterState(false); assertPlayerClockState(() => player1, false); assertPlayerClockState(() => player2, false); setWaiting(() => player2, false); - assertMasterState(true); assertPlayerClockState(() => player1, true); assertPlayerClockState(() => player2, true); } [Test] - public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime() - { - AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); - assertMasterState(false); - } - - [Test] - public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime() + public void TestReadyPlayersStartWhenReadyForMaximumDelayTime() { setWaiting(() => player1, false); AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); - assertMasterState(true); + assertPlayerClockState(() => player1, true); + assertPlayerClockState(() => player2, false); } [Test] @@ -153,9 +145,6 @@ namespace osu.Game.Tests.OnlinePlay private void setPlayerClockTime(Func playerClock, double offsetFromMaster) => AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster)); - private void assertMasterState(bool running) - => AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running); - private void assertCatchingUp(Func playerClock, bool catchingUp) => AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp); @@ -201,6 +190,11 @@ namespace osu.Game.Tests.OnlinePlay private class TestManualClock : ManualClock, IAdjustableClock { + public TestManualClock() + { + IsRunning = true; + } + public void Start() => IsRunning = true; public void Stop() => IsRunning = false; diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs new file mode 100644 index 0000000000..28ad7ed6a7 --- /dev/null +++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs @@ -0,0 +1,89 @@ +// 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.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; +using osu.Framework.Testing; +using osu.Game.Audio; +using osu.Game.Rulesets; +using osu.Game.Skinning; +using osu.Game.Tests.Testing; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Rulesets +{ + [HeadlessTest] + public class TestSceneRulesetSkinProvidingContainer : OsuTestScene + { + private SkinRequester requester; + + protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset(); + + [Test] + public void TestRulesetResources() + { + setupProviderStep(); + + AddAssert("ruleset texture retrieved via skin", () => requester.GetTexture("test-image") != null); + AddAssert("ruleset sample retrieved via skin", () => requester.GetSample(new SampleInfo("test-sample")) != null); + } + + [Test] + public void TestEarlyAddedSkinRequester() + { + Texture textureOnLoad = null; + + AddStep("setup provider", () => + { + var rulesetSkinProvider = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin); + + rulesetSkinProvider.Add(requester = new SkinRequester()); + + requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture("test-image"); + + Child = rulesetSkinProvider; + }); + + AddAssert("requester got correct initial texture", () => textureOnLoad != null); + } + + private void setupProviderStep() + { + AddStep("setup provider", () => + { + Child = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin) + .WithChild(requester = new SkinRequester()); + }); + } + + private class SkinRequester : Drawable, ISkin + { + private ISkinSource skin; + + public event Action OnLoadAsync; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + this.skin = skin; + + OnLoadAsync?.Invoke(); + } + + public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component); + + public Texture GetTexture(string componentName, WrapMode wrapModeS = default, WrapMode wrapModeT = default) => skin.GetTexture(componentName); + + public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo); + + public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup); + } + } +} diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 7522aca5dc..cd7d744f53 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Scores.IO OnlineScoreID = 12345, }; - var imported = await loadScoreIntoOsu(osu, toImport); + var imported = await LoadScoreIntoOsu(osu, toImport); Assert.AreEqual(toImport.Rank, imported.Rank); Assert.AreEqual(toImport.TotalScore, imported.TotalScore); @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Scores.IO Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, }; - var imported = await loadScoreIntoOsu(osu, toImport); + var imported = await LoadScoreIntoOsu(osu, toImport); Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime)); @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Scores.IO } }; - var imported = await loadScoreIntoOsu(osu, toImport); + var imported = await LoadScoreIntoOsu(osu, toImport); Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]); Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]); @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Scores.IO } }; - var imported = await loadScoreIntoOsu(osu, toImport); + var imported = await LoadScoreIntoOsu(osu, toImport); var beatmapManager = osu.Dependencies.Get(); var scoreManager = osu.Dependencies.Get(); @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Scores.IO beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID))); Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true)); - var secondImport = await loadScoreIntoOsu(osu, imported); + var secondImport = await LoadScoreIntoOsu(osu, imported); Assert.That(secondImport, Is.Null); } finally @@ -163,7 +163,7 @@ namespace osu.Game.Tests.Scores.IO { var osu = LoadOsuIntoHost(host, true); - await loadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader()); + await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader()); var scoreManager = osu.Dependencies.Get(); @@ -177,7 +177,7 @@ namespace osu.Game.Tests.Scores.IO } } - private async Task loadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) + public static async Task LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) { var beatmapManager = osu.Dependencies.Get(); @@ -190,7 +190,7 @@ namespace osu.Game.Tests.Scores.IO return scoreManager.GetAllUsableScores().FirstOrDefault(); } - private class TestArchiveReader : ArchiveReader + internal class TestArchiveReader : ArchiveReader { public TestArchiveReader() : base("test_archive") diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs index 97087e31ab..fb50da32f3 100644 --- a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs +++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Testing Dependencies.Get() != null); } - private class TestRuleset : Ruleset + public class TestRuleset : Ruleset { public override string Description => string.Empty; public override string ShortName => string.Empty; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 6eeb3596a8..7584d67afe 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using System.Threading; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -41,8 +39,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuGameBase game { get; set; } - private int nextFrame; - private BeatmapSetInfo importedBeatmap; private int importedBeatmapId; @@ -51,8 +47,6 @@ namespace osu.Game.Tests.Visual.Gameplay { base.SetUpSteps(); - AddStep("reset sent frames", () => nextFrame = 0); - AddStep("import beatmap", () => { importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result; @@ -105,7 +99,8 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); checkPaused(true); - sendFrames(1000); // send enough frames to ensure play won't be paused + // send enough frames to ensure play won't be paused + sendFrames(100); checkPaused(false); } @@ -114,12 +109,12 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestSpectatingDuringGameplay() { start(); + sendFrames(300); loadSpectatingScreen(); waitForPlayer(); - AddStep("advance frame count", () => nextFrame = 300); - sendFrames(); + sendFrames(300); AddUntilStep("playing from correct point in time", () => player.ChildrenOfType().First().FrameStableClock.CurrentTime > 30000); } @@ -220,11 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void sendFrames(int count = 10) { - AddStep("send frames", () => - { - testSpectatorClient.SendFrames(streamingUser.Id, nextFrame, count); - nextFrame += count; - }); + AddStep("send frames", () => testSpectatorClient.SendFrames(streamingUser.Id, count)); } private void loadSpectatingScreen() @@ -232,14 +223,5 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser))); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); } - - internal class TestUserLookupCache : UserLookupCache - { - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User - { - Id = lookup, - Username = $"User {lookup}" - }); - } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs deleted file mode 100644 index c665a57452..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Online.Rooms; -using osu.Game.Rulesets; -using osu.Game.Screens.OnlinePlay; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public abstract class RoomManagerTestScene : RoomTestScene - { - [Cached(Type = typeof(IRoomManager))] - protected TestRoomManager RoomManager { get; } = new TestRoomManager(); - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("clear rooms", () => RoomManager.Rooms.Clear()); - } - - protected void AddRooms(int count, RulesetInfo ruleset = null) - { - AddStep("add rooms", () => - { - for (int i = 0; i < count; i++) - { - var room = new Room - { - RoomID = { Value = i }, - Name = { Value = $"Room {i}" }, - Host = { Value = new User { Username = "Host" } }, - EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }, - Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal } - }; - - if (ruleset != null) - { - room.Playlist.Add(new PlaylistItem - { - Ruleset = { Value = ruleset }, - Beatmap = - { - Value = new BeatmapInfo - { - Metadata = new BeatmapMetadata() - } - } - }); - } - - RoomManager.Rooms.Add(room); - } - }); - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs deleted file mode 100644 index 1785c99784..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Bindables; -using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestRoomManager : IRoomManager - { - public event Action RoomsUpdated - { - add { } - remove { } - } - - public readonly BindableList Rooms = new BindableList(); - - public IBindable InitialRoomsReceived { get; } = new Bindable(true); - - IBindableList IRoomManager.Rooms => Rooms; - - public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room); - - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) - { - } - - public void PartRoom() - { - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs index 9f24347ae9..471d0b6c98 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs @@ -4,17 +4,21 @@ using System; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneLoungeRoomInfo : RoomTestScene + public class TestSceneLoungeRoomInfo : OnlinePlayTestScene { [SetUp] public new void Setup() => Schedule(() => { + SelectedRoom.Value = new Room(); + Child = new RoomInfo { Anchor = Anchor.Centre, @@ -23,15 +27,10 @@ namespace osu.Game.Tests.Visual.Multiplayer }; }); - public override void SetUpSteps() - { - // Todo: Temp - } - [Test] public void TestNonSelectedRoom() { - AddStep("set null room", () => Room.RoomID.Value = null); + AddStep("set null room", () => SelectedRoom.Value.RoomID.Value = null); } [Test] @@ -39,11 +38,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set open room", () => { - Room.RoomID.Value = 0; - Room.Name.Value = "Room 0"; - Room.Host.Value = new User { Username = "peppy", Id = 2 }; - Room.EndDate.Value = DateTimeOffset.Now.AddMonths(1); - Room.Status.Value = new RoomStatusOpen(); + SelectedRoom.Value.RoomID.Value = 0; + SelectedRoom.Value.Name.Value = "Room 0"; + SelectedRoom.Value.Host.Value = new User { Username = "peppy", Id = 2 }; + SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMonths(1); + SelectedRoom.Value.Status.Value = new RoomStatusOpen(); }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 5682fd5c3c..75cc687ee8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -3,24 +3,26 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneLoungeRoomsContainer : RoomManagerTestScene + public class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { + protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager; + private RoomsContainer container; - [BackgroundDependencyLoader] - private void load() + [SetUp] + public new void Setup() => Schedule(() => { Child = container = new RoomsContainer { @@ -29,12 +31,12 @@ namespace osu.Game.Tests.Visual.Multiplayer Width = 0.5f, JoinRequested = joinRequested }; - } + }); [Test] public void TestBasicListChanges() { - AddRooms(3); + AddStep("add rooms", () => RoomManager.AddRooms(3)); AddAssert("has 3 rooms", () => container.Rooms.Count == 3); AddStep("remove first room", () => RoomManager.Rooms.Remove(RoomManager.Rooms.FirstOrDefault())); @@ -51,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestKeyboardNavigation() { - AddRooms(3); + AddStep("add rooms", () => RoomManager.AddRooms(3)); AddAssert("no selection", () => checkRoomSelected(null)); @@ -72,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestClickDeselection() { - AddRooms(1); + AddStep("add room", () => RoomManager.AddRooms(1)); AddAssert("no selection", () => checkRoomSelected(null)); @@ -91,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestStringFiltering() { - AddRooms(4); + AddStep("add rooms", () => RoomManager.AddRooms(4)); AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); @@ -107,21 +109,21 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestRulesetFiltering() { - AddRooms(2, new OsuRuleset().RulesetInfo); - AddRooms(3, new CatchRuleset().RulesetInfo); + AddStep("add rooms", () => RoomManager.AddRooms(2, new OsuRuleset().RulesetInfo)); + AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo)); + // Todo: What even is this case...? + AddStep("set empty filter criteria", () => container.Filter(null)); AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5); AddStep("filter osu! rooms", () => container.Filter(new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo })); - AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2); AddStep("filter catch rooms", () => container.Filter(new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo })); - AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3); } - private bool checkRoomSelected(Room room) => Room == room; + private bool checkRoomSelected(Room room) => SelectedRoom.Value == room; private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 9ad9f2c883..d66603a448 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -11,11 +11,12 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.OnlinePlay; using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchBeatmapDetailArea : RoomTestScene + public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene { [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -26,6 +27,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public new void Setup() => Schedule(() => { + SelectedRoom.Value = new Room(); + Child = new MatchBeatmapDetailArea { Anchor = Anchor.Centre, @@ -37,9 +40,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createNewItem() { - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem { - ID = Room.Playlist.Count, + ID = SelectedRoom.Value.Playlist.Count, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, RequiredMods = diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs index 7cdc6b1a7d..71ba5db481 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs @@ -7,46 +7,49 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchHeader : RoomTestScene + public class TestSceneMatchHeader : OnlinePlayTestScene { - public TestSceneMatchHeader() - { - Child = new Header(); - } - [SetUp] public new void Setup() => Schedule(() => { - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value = new Room { - Beatmap = + Name = { Value = "A very awesome room" }, + Host = { Value = new User { Id = 2, Username = "peppy" } }, + Playlist = { - Value = new BeatmapInfo + new PlaylistItem { - Metadata = new BeatmapMetadata + Beatmap = { - Title = "Title", - Artist = "Artist", - AuthorString = "Author", + Value = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Title = "Title", + Artist = "Artist", + AuthorString = "Author", + }, + Version = "Version", + Ruleset = new OsuRuleset().RulesetInfo + } }, - Version = "Version", - Ruleset = new OsuRuleset().RulesetInfo + RequiredMods = + { + new OsuModDoubleTime(), + new OsuModNoFail(), + new OsuModRelax(), + } } - }, - RequiredMods = - { - new OsuModDoubleTime(), - new OsuModNoFail(), - new OsuModRelax(), } - }); + }; - Room.Name.Value = "A very awesome room"; - Room.Host.Value = new User { Id = 2, Username = "peppy" }; + Child = new Header(); }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index 64eaf0556b..a7a5f3af39 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -2,72 +2,74 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchLeaderboard : RoomTestScene + public class TestSceneMatchLeaderboard : OnlinePlayTestScene { - protected override bool UseOnlineAPI => true; - - public TestSceneMatchLeaderboard() - { - Add(new MatchLeaderboard - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Size = new Vector2(550f, 450f), - Scope = MatchLeaderboardScope.Overall, - }); - } - [BackgroundDependencyLoader] - private void load(IAPIProvider api) + private void load() { - var req = new GetRoomScoresRequest(); - req.Success += v => { }; - req.Failure += _ => { }; + ((DummyAPIAccess)API).HandleRequest = r => + { + switch (r) + { + case GetRoomLeaderboardRequest leaderboardRequest: + leaderboardRequest.TriggerSuccess(new APILeaderboard + { + Leaderboard = new List + { + new APIUserScoreAggregate + { + UserID = 2, + User = new User { Id = 2, Username = "peppy" }, + TotalScore = 995533, + RoomID = 3, + CompletedBeatmaps = 1, + TotalAttempts = 6, + Accuracy = 0.9851 + }, + new APIUserScoreAggregate + { + UserID = 1040328, + User = new User { Id = 1040328, Username = "smoogipoo" }, + TotalScore = 981100, + RoomID = 3, + CompletedBeatmaps = 1, + TotalAttempts = 9, + Accuracy = 0.937 + } + } + }); + return true; + } - api.Queue(req); + return false; + }; } [SetUp] public new void Setup() => Schedule(() => { - Room.RoomID.Value = 3; + SelectedRoom.Value = new Room { RoomID = { Value = 3 } }; + + Child = new MatchLeaderboard + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(550f, 450f), + Scope = MatchLeaderboardScope.Overall, + }; }); - - private class GetRoomScoresRequest : APIRequest> - { - protected override string Target => "rooms/3/leaderboard"; - } - - private class RoomScore - { - [JsonProperty("user")] - public User User { get; set; } - - [JsonProperty("accuracy")] - public double Accuracy { get; set; } - - [JsonProperty("total_score")] - public int TotalScore { get; set; } - - [JsonProperty("pp")] - public double PP { get; set; } - - [JsonProperty("attempts")] - public int TotalAttempts { get; set; } - - [JsonProperty("completed")] - public int CompletedAttempts { get; set; } - } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 5ad35be0ec..e14df62af1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -3,74 +3,40 @@ using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Timing; -using osu.Game.Database; -using osu.Game.Online.Spectator; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play.HUD; -using osu.Game.Tests.Visual.Spectator; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene { - [Cached(typeof(SpectatorClient))] - private TestSpectatorClient spectatorClient = new TestSpectatorClient(); - - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestUserLookupCache(); - - protected override Container Content => content; - private readonly Container content; - - private readonly Dictionary clocks = new Dictionary - { - { PLAYER_1_ID, new ManualClock() }, - { PLAYER_2_ID, new ManualClock() } - }; - - public TestSceneMultiSpectatorLeaderboard() - { - base.Content.AddRange(new Drawable[] - { - spectatorClient, - lookupCache, - content = new Container { RelativeSizeAxes = Axes.Both } - }); - } + private Dictionary clocks; + private MultiSpectatorLeaderboard leaderboard; [SetUpSteps] public new void SetUpSteps() { - MultiSpectatorLeaderboard leaderboard = null; - AddStep("reset", () => { Clear(); - foreach (var (userId, clock) in clocks) + clocks = new Dictionary { - spectatorClient.EndPlay(userId); - clock.CurrentTime = 0; - } + { PLAYER_1_ID, new ManualClock() }, + { PLAYER_2_ID, new ManualClock() } + }; + + foreach (var (userId, _) in clocks) + SpectatorClient.StartPlay(userId, 0); }); AddStep("create leaderboard", () => { - foreach (var (userId, _) in clocks) - spectatorClient.StartPlay(userId, 0); - Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); var scoreProcessor = new OsuScoreProcessor(); scoreProcessor.ApplyBeatmap(playable); @@ -96,10 +62,10 @@ namespace osu.Game.Tests.Visual.Multiplayer // For player 2, send frames in sets of 10. for (int i = 0; i < 100; i++) { - spectatorClient.SendFrames(PLAYER_1_ID, i, 1); + SpectatorClient.SendFrames(PLAYER_1_ID, 1); if (i % 10 == 0) - spectatorClient.SendFrames(PLAYER_2_ID, i, 10); + SpectatorClient.SendFrames(PLAYER_2_ID, 10); } }); @@ -145,17 +111,5 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertCombo(int userId, int expectedCombo) => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo); - - private class TestUserLookupCache : UserLookupCache - { - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) - { - return Task.FromResult(new User - { - Id = lookup, - Username = $"User {lookup}" - }); - } - } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index b91391c409..b8db4067fb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -3,31 +3,20 @@ using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Online.Spectator; +using osu.Game.Rulesets.UI; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; -using osu.Game.Tests.Visual.Spectator; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiSpectatorScreen : MultiplayerTestScene { - [Cached(typeof(SpectatorClient))] - private TestSpectatorClient spectatorClient = new TestSpectatorClient(); - - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestUserLookupCache(); - [Resolved] private OsuGameBase game { get; set; } @@ -37,7 +26,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private MultiSpectatorScreen spectatorScreen; private readonly List playingUserIds = new List(); - private readonly Dictionary nextFrame = new Dictionary(); private BeatmapSetInfo importedSet; private BeatmapInfo importedBeatmap; @@ -51,25 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer importedBeatmapId = importedBeatmap.OnlineBeatmapID ?? -1; } - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("reset sent frames", () => nextFrame.Clear()); - - AddStep("add streaming client", () => - { - Remove(spectatorClient); - Add(spectatorClient); - }); - - AddStep("finish previous gameplay", () => - { - foreach (var id in playingUserIds) - spectatorClient.EndPlay(id); - playingUserIds.Clear(); - }); - } + [SetUp] + public new void Setup() => Schedule(() => playingUserIds.Clear()); [Test] public void TestDelayedStart() @@ -80,18 +51,16 @@ namespace osu.Game.Tests.Visual.Multiplayer Client.CurrentMatchPlayingUserIds.Add(PLAYER_2_ID); playingUserIds.Add(PLAYER_1_ID); playingUserIds.Add(PLAYER_2_ID); - nextFrame[PLAYER_1_ID] = 0; - nextFrame[PLAYER_2_ID] = 0; }); loadSpectateScreen(false); AddWaitStep("wait a bit", 10); - AddStep("load player first_player_id", () => spectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId)); + AddStep("load player first_player_id", () => SpectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId)); AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType().Count() == 1); AddWaitStep("wait a bit", 10); - AddStep("load player second_player_id", () => spectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId)); + AddStep("load player second_player_id", () => SpectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId)); AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType().Count() == 2); } @@ -107,6 +76,23 @@ namespace osu.Game.Tests.Visual.Multiplayer AddWaitStep("wait a bit", 20); } + [Test] + public void TestTimeDoesNotProgressWhileAllPlayersPaused() + { + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); + loadSpectateScreen(); + + sendFrames(PLAYER_1_ID, 40); + sendFrames(PLAYER_2_ID, 20); + + checkPaused(PLAYER_2_ID, true); + checkPausedInstant(PLAYER_1_ID, false); + AddAssert("master clock still running", () => this.ChildrenOfType().Single().IsRunning); + + checkPaused(PLAYER_1_ID, true); + AddUntilStep("master clock paused", () => !this.ChildrenOfType().Single().IsRunning); + } + [Test] public void TestPlayersMustStartSimultaneously() { @@ -182,7 +168,7 @@ namespace osu.Game.Tests.Visual.Multiplayer // Send initial frames for both players. A few more for player 1. sendFrames(PLAYER_1_ID, 1000); - sendFrames(PLAYER_2_ID, 10); + sendFrames(PLAYER_2_ID, 30); checkPausedInstant(PLAYER_1_ID, false); checkPausedInstant(PLAYER_2_ID, false); @@ -210,8 +196,8 @@ namespace osu.Game.Tests.Visual.Multiplayer sendFrames(PLAYER_1_ID, 10); sendFrames(PLAYER_2_ID, 20); - assertMuted(PLAYER_1_ID, false); - assertMuted(PLAYER_2_ID, true); + checkPaused(PLAYER_1_ID, false); + assertOneNotMuted(); checkPaused(PLAYER_1_ID, true); assertMuted(PLAYER_1_ID, true); @@ -229,6 +215,36 @@ namespace osu.Game.Tests.Visual.Multiplayer assertMuted(PLAYER_2_ID, true); } + [Test] + public void TestSpectatingDuringGameplay() + { + var players = new[] { PLAYER_1_ID, PLAYER_2_ID }; + + start(players); + sendFrames(players, 300); + + loadSpectateScreen(); + sendFrames(players, 300); + + AddUntilStep("playing from correct point in time", () => this.ChildrenOfType().All(r => r.FrameStableClock.CurrentTime > 30000)); + } + + [Test] + public void TestSpectatingDuringGameplayWithLateFrames() + { + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); + sendFrames(new[] { PLAYER_1_ID, PLAYER_2_ID }, 300); + + loadSpectateScreen(); + sendFrames(PLAYER_1_ID, 300); + + AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + checkPaused(PLAYER_1_ID, false); + + sendFrames(PLAYER_2_ID, 300); + AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType().Single().FrameStableClock.CurrentTime > 30000); + } + private void loadSpectateScreen(bool waitForPlayerLoad = true) { AddStep("load screen", () => @@ -242,8 +258,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); } - private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId); - private void start(int[] userIds, int? beatmapId = null) { AddStep("start play", () => @@ -251,23 +265,12 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (int id in userIds) { Client.CurrentMatchPlayingUserIds.Add(id); - spectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId); + SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId); playingUserIds.Add(id); - nextFrame[id] = 0; } }); } - private void finish(int userId) - { - AddStep("end play", () => - { - spectatorClient.EndPlay(userId); - playingUserIds.Remove(userId); - nextFrame.Remove(userId); - }); - } - private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count); private void sendFrames(int[] userIds, int count = 10) @@ -275,10 +278,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("send frames", () => { foreach (int id in userIds) - { - spectatorClient.SendFrames(id, nextFrame[id], count); - nextFrame[id] += count; - } + SpectatorClient.SendFrames(id, count); }); } @@ -286,7 +286,14 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); private void checkPausedInstant(int userId, bool state) - => AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); + { + checkPaused(userId, state); + + // Todo: The following should work, but is broken because SpectatorScreen retrieves the WorkingBeatmap via the BeatmapManager, bypassing the test scene clock and running real-time. + // AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); + } + + private void assertOneNotMuted() => AddAssert("one player not muted", () => spectatorScreen.ChildrenOfType().Count(p => !p.Mute) == 1); private void assertMuted(int userId, bool muted) => AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted); @@ -297,17 +304,5 @@ namespace osu.Game.Tests.Visual.Multiplayer private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single(); private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId); - - internal class TestUserLookupCache : UserLookupCache - { - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) - { - return Task.FromResult(new User - { - Id = lookup, - Username = $"User {lookup}" - }); - } - } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 599dfb082b..c93640e7b5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -18,6 +18,7 @@ using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; @@ -30,14 +31,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayer : ScreenTestScene { - private TestMultiplayer multiplayerScreen; - private BeatmapManager beatmaps; private RulesetStore rulesets; private BeatmapSetInfo importedSet; - private TestMultiplayerClient client => multiplayerScreen.Client; - private Room room => client.APIRoom; + private DependenciesScreen dependenciesScreen; + private TestMultiplayer multiplayerScreen; + private TestMultiplayerClient client; public TestSceneMultiplayer() { @@ -229,30 +229,43 @@ namespace osu.Game.Tests.Visual.Multiplayer private void loadMultiplayer() { - AddStep("show", () => + AddStep("create multiplayer screen", () => multiplayerScreen = new TestMultiplayer()); + + AddStep("load dependencies", () => { - multiplayerScreen = new TestMultiplayer(); + client = new TestMultiplayerClient(multiplayerScreen.RoomManager); - // Needs to be added at a higher level since the multiplayer screen becomes non-current. - Child = multiplayerScreen.Client; + // The screen gets suspended so it stops receiving updates. + Child = client; - LoadScreen(multiplayerScreen); + LoadScreen(dependenciesScreen = new DependenciesScreen(client)); }); - AddUntilStep("wait for loaded", () => multiplayerScreen.IsLoaded); + AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); + + AddStep("load multiplayer", () => LoadScreen(multiplayerScreen)); + AddUntilStep("wait for multiplayer to load", () => multiplayerScreen.IsLoaded); } - private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer + /// + /// Used for the sole purpose of adding as a resolvable dependency. + /// + private class DependenciesScreen : OsuScreen { [Cached(typeof(MultiplayerClient))] public readonly TestMultiplayerClient Client; - public TestMultiplayer() + public DependenciesScreen(TestMultiplayerClient client) { - Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager); + Client = client; } + } - protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); + private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer + { + public new TestMultiplayerRoomManager RoomManager { get; private set; } + + protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager(); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index af2f6fa5fe..0e368b59dd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -6,12 +6,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; -using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; @@ -19,37 +18,20 @@ using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; -using osu.Game.Tests.Visual.Online; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene { - private const int users = 16; + private static IEnumerable users => Enumerable.Range(0, 16); - [Cached(typeof(SpectatorClient))] - private TestMultiplayerSpectatorClient spectatorClient = new TestMultiplayerSpectatorClient(); - - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); + public new TestMultiplayerSpectatorClient SpectatorClient => (TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient; private MultiplayerGameplayLeaderboard leaderboard; - - protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; - private OsuConfigManager config; - public TestSceneMultiplayerGameplayLeaderboard() - { - base.Content.Children = new Drawable[] - { - spectatorClient, - lookupCache, - Content - }; - } - [BackgroundDependencyLoader] private void load() { @@ -59,7 +41,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUpSteps] public override void SetUpSteps() { - AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = lookupCache.GetUserAsync(1).Result); + AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result); AddStep("create leaderboard", () => { @@ -70,14 +52,11 @@ namespace osu.Game.Tests.Visual.Multiplayer var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); - for (int i = 0; i < users; i++) - spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); + foreach (var user in users) + SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); - spectatorClient.Schedule(() => - { - Client.CurrentMatchPlayingUserIds.Clear(); - Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers); - }); + // Todo: This is REALLY bad. + Client.CurrentMatchPlayingUserIds.AddRange(users); Children = new Drawable[] { @@ -86,7 +65,7 @@ namespace osu.Game.Tests.Visual.Multiplayer scoreProcessor.ApplyBeatmap(playable); - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, spectatorClient.PlayingUsers.ToArray()) + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, users.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -100,24 +79,32 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestScoreUpdates() { - AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 100); + AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100); AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded); } [Test] public void TestUserQuit() { - AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users); + foreach (var user in users) + AddStep($"mark user {user} quit", () => Client.RemoveUser(LookupCache.GetUserAsync(user).Result.AsNonNull())); } [Test] public void TestChangeScoringMode() { - AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 5); + AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 5); AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic)); AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); } + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); + + protected class TestDependencies : MultiplayerTestSceneDependencies + { + protected override TestSpectatorClient CreateSpectatorClient() => new TestMultiplayerSpectatorClient(); + } + public class TestMultiplayerSpectatorClient : TestSpectatorClient { private readonly Dictionary lastHeaders = new Dictionary(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs index 6b03b53b4b..4e08ffef17 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.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.Framework.Allocation; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; @@ -10,18 +10,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerMatchFooter : MultiplayerTestScene { - [Cached] - private readonly OnlinePlayBeatmapAvailabilityTracker availablilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); - - [BackgroundDependencyLoader] - private void load() + [SetUp] + public new void Setup() => Schedule(() => { + SelectedRoom.Value = new Room(); + Child = new MultiplayerMatchFooter { Anchor = Anchor.Centre, Origin = Anchor.Centre, Height = 50 }; - } + }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 5b059c06f5..8bcb9cebbc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -29,7 +29,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayerMatchSongSelect : RoomTestScene + public class TestSceneMultiplayerMatchSongSelect : MultiplayerTestScene { private BeatmapManager manager; private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e8ebc0c426..955be6ca21 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -49,13 +49,13 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public new void Setup() => Schedule(() => { - Room.Name.Value = "Test Room"; + SelectedRoom.Value = new Room { Name = { Value = "Test Room" } }; }); [SetUpSteps] public void SetupSteps() { - AddStep("load match", () => LoadScreen(screen = new MultiplayerMatchSubScreen(Room))); + AddStep("load match", () => LoadScreen(screen = new MultiplayerMatchSubScreen(SelectedRoom.Value))); AddUntilStep("wait for load", () => screen.IsCurrentScreen()); } @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set playlist", () => { - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set playlist", () => { - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set playlist", () => { - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 7f8f04b718..6526f7eea7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -22,8 +22,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene { - [SetUp] - public new void Setup() => Schedule(createNewParticipantsList); + [SetUpSteps] + public void SetupSteps() + { + createNewParticipantsList(); + } [Test] public void TestAddUser() @@ -88,7 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestCorrectInitialState() { AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); - AddStep("recreate list", createNewParticipantsList); + createNewParticipantsList(); checkProgressBarVisibility(true); } @@ -233,7 +236,17 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createNewParticipantsList() { - Child = new ParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Size = new Vector2(380, 0.7f) }; + ParticipantsList participantsList = null; + + AddStep("create new list", () => Child = participantsList = new ParticipantsList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(380, 0.7f) + }); + + AddUntilStep("wait for list to load", () => participantsList.IsLoaded); } private void checkProgressBarVisibility(bool visible) => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 929cd6ca80..4f2ca34fb0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -27,7 +27,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneMultiplayerReadyButton : MultiplayerTestScene { private MultiplayerReadyButton button; - private OnlinePlayBeatmapAvailabilityTracker beatmapTracker; private BeatmapSetInfo importedSet; private readonly Bindable selectedItem = new Bindable(); @@ -43,18 +42,13 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); - - Add(beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker - { - SelectedItem = { BindTarget = selectedItem } - }); - - Dependencies.Cache(beatmapTracker); } [SetUp] public new void Setup() => Schedule(() => { + AvailabilityTracker.SelectedItem.BindTo(selectedItem); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); selectedItem.Value = new PlaylistItem diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs index c008771fd9..b17427a30b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs @@ -3,37 +3,38 @@ using System; using NUnit.Framework; -using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { [HeadlessTest] - public class TestSceneMultiplayerRoomManager : RoomTestScene + public class TestSceneMultiplayerRoomManager : MultiplayerTestScene { - private TestMultiplayerRoomContainer roomContainer; - private TestMultiplayerRoomManager roomManager => roomContainer.RoomManager; + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); + + public TestSceneMultiplayerRoomManager() + : base(false) + { + } [Test] public void TestPollsInitially() { AddStep("create room manager with a few rooms", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom(r => r.Name.Value = "1")); - roomManager.PartRoom(); - roomManager.CreateRoom(createRoom(r => r.Name.Value = "2")); - roomManager.PartRoom(); - roomManager.ClearRooms(); - }); + RoomManager.CreateRoom(createRoom(r => r.Name.Value = "1")); + RoomManager.PartRoom(); + RoomManager.CreateRoom(createRoom(r => r.Name.Value = "2")); + RoomManager.PartRoom(); + RoomManager.ClearRooms(); }); - AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2); - AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); + AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2); + AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value); } [Test] @@ -41,19 +42,16 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a few rooms", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom()); - roomManager.PartRoom(); - roomManager.CreateRoom(createRoom()); - roomManager.PartRoom(); - }); + RoomManager.CreateRoom(createRoom()); + RoomManager.PartRoom(); + RoomManager.CreateRoom(createRoom()); + RoomManager.PartRoom(); }); - AddStep("disconnect", () => roomContainer.Client.Disconnect()); + AddStep("disconnect", () => Client.Disconnect()); - AddAssert("rooms cleared", () => ((RoomManager)roomManager).Rooms.Count == 0); - AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); + AddAssert("rooms cleared", () => ((RoomManager)RoomManager).Rooms.Count == 0); + AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value); } [Test] @@ -61,20 +59,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a few rooms", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom()); - roomManager.PartRoom(); - roomManager.CreateRoom(createRoom()); - roomManager.PartRoom(); - }); + RoomManager.CreateRoom(createRoom()); + RoomManager.PartRoom(); + RoomManager.CreateRoom(createRoom()); + RoomManager.PartRoom(); }); - AddStep("disconnect", () => roomContainer.Client.Disconnect()); - AddStep("connect", () => roomContainer.Client.Connect()); + AddStep("disconnect", () => Client.Disconnect()); + AddStep("connect", () => Client.Connect()); - AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2); - AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); + AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2); + AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value); } [Test] @@ -82,15 +77,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a room", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom()); - roomManager.ClearRooms(); - }); + RoomManager.CreateRoom(createRoom()); + RoomManager.ClearRooms(); }); - AddAssert("manager not polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 0); - AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); + AddAssert("manager not polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 0); + AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value); } [Test] @@ -98,13 +90,10 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a room", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom()); - }); + RoomManager.CreateRoom(createRoom()); }); - AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null); + AddUntilStep("multiplayer room joined", () => Client.Room != null); } [Test] @@ -112,14 +101,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a room", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom()); - roomManager.PartRoom(); - }); + RoomManager.CreateRoom(createRoom()); + RoomManager.PartRoom(); }); - AddAssert("multiplayer room parted", () => roomContainer.Client.Room == null); + AddAssert("multiplayer room parted", () => Client.Room == null); } [Test] @@ -127,16 +113,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a room", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - var r = createRoom(); - roomManager.CreateRoom(r); - roomManager.PartRoom(); - roomManager.JoinRoom(r); - }); + var r = createRoom(); + RoomManager.CreateRoom(r); + RoomManager.PartRoom(); + RoomManager.JoinRoom(r); }); - AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null); + AddUntilStep("multiplayer room joined", () => Client.Room != null); } private Room createRoom(Action initFunc = null) @@ -161,18 +144,14 @@ namespace osu.Game.Tests.Visual.Multiplayer return room; } - private TestMultiplayerRoomManager createRoomManager() + private class TestDependencies : MultiplayerTestSceneDependencies { - Child = roomContainer = new TestMultiplayerRoomContainer + public TestDependencies() { - RoomManager = - { - TimeBetweenListingPolls = { Value = 1 }, - TimeBetweenSelectionPolls = { Value = 1 } - } - }; - - return roomManager; + // Need to set these values as early as possible. + RoomManager.TimeBetweenListingPolls.Value = 1; + RoomManager.TimeBetweenSelectionPolls.Value = 1; + } } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index d00404102c..070158f552 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -37,40 +37,19 @@ namespace osu.Game.Tests.Visual.Multiplayer private IDisposable readyClickOperation; - protected override Container Content => content; - private readonly Container content; - - public TestSceneMultiplayerSpectateButton() - { - base.Content.Add(content = new Container - { - RelativeSizeAxes = Axes.Both - }); - } - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - - return dependencies; - } - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - - var beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker { SelectedItem = { BindTarget = selectedItem } }; - base.Content.Add(beatmapTracker); - Dependencies.Cache(beatmapTracker); - beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); } [SetUp] public new void Setup() => Schedule(() => { + AvailabilityTracker.SelectedItem.BindTo(selectedItem); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); selectedItem.Value = new PlaylistItem diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index d95a95ebe5..e4bf9b36ed 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -14,16 +14,18 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestScenePlaylistsSongSelect : RoomTestScene + public class TestScenePlaylistsSongSelect : OnlinePlayTestScene { [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -85,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("reset", () => { + SelectedRoom.Value = new Room(); Ruleset.Value = new OsuRuleset().RulesetInfo; Beatmap.SetDefault(); SelectedMods.Value = Array.Empty(); @@ -98,14 +101,14 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestItemAddedIfEmptyOnStart() { AddStep("finalise selection", () => songSelect.FinaliseSelection()); - AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1); } [Test] public void TestItemAddedWhenCreateNewItemClicked() { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); - AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1); } [Test] @@ -113,7 +116,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); AddStep("finalise selection", () => songSelect.FinaliseSelection()); - AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1); } [Test] @@ -121,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); - AddAssert("playlist has 2 items", () => Room.Playlist.Count == 2); + AddAssert("playlist has 2 items", () => SelectedRoom.Value.Playlist.Count == 2); } [Test] @@ -131,13 +134,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); AddStep("rearrange", () => { - var item = Room.Playlist[0]; - Room.Playlist.RemoveAt(0); - Room.Playlist.Add(item); + var item = SelectedRoom.Value.Playlist[0]; + SelectedRoom.Value.Playlist.RemoveAt(0); + SelectedRoom.Value.Playlist.Add(item); }); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); - AddAssert("new item has id 2", () => Room.Playlist.Last().ID == 2); + AddAssert("new item has id 2", () => SelectedRoom.Value.Playlist.Last().ID == 2); } /// @@ -151,8 +154,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2); AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); - AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value)); - AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.Last().RequiredMods[0]).SpeedChange.Value)); } /// @@ -174,7 +177,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2); - AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value)); } private class TestPlaylistsSongSelect : PlaylistsSongSelect diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 156d6b744e..5bfb676f81 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -14,6 +15,8 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Users; namespace osu.Game.Tests.Visual.Online { @@ -23,6 +26,8 @@ namespace osu.Game.Tests.Visual.Online private BeatmapListingOverlay overlay; + private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType().Single(); + [BackgroundDependencyLoader] private void load() { @@ -39,6 +44,16 @@ namespace osu.Game.Tests.Visual.Online return true; }; + + AddStep("initialize dummy", () => + { + // non-supporter user + ((DummyAPIAccess)API).LocalUser.Value = new User + { + Username = "TestBot", + Id = API.LocalUser.Value.Id + 1, + }; + }); } [Test] @@ -58,13 +73,164 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); } + [Test] + public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithoutResults() + { + AddStep("fetch for 0 beatmaps", () => fetchFor()); + AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); + + // only Rank Achieved filter + setRankAchievedFilter(new[] { ScoreRank.XH }); + supporterRequiredPlaceholderShown(); + + setRankAchievedFilter(Array.Empty()); + notFoundPlaceholderShown(); + + // only Played filter + setPlayedFilter(SearchPlayed.Played); + supporterRequiredPlaceholderShown(); + + setPlayedFilter(SearchPlayed.Any); + notFoundPlaceholderShown(); + + // both RankAchieved and Played filters + setRankAchievedFilter(new[] { ScoreRank.XH }); + setPlayedFilter(SearchPlayed.Played); + supporterRequiredPlaceholderShown(); + + setRankAchievedFilter(Array.Empty()); + setPlayedFilter(SearchPlayed.Any); + notFoundPlaceholderShown(); + } + + [Test] + public void TestUserWithSupporterUsesSupporterOnlyFiltersWithoutResults() + { + AddStep("fetch for 0 beatmaps", () => fetchFor()); + AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); + + // only Rank Achieved filter + setRankAchievedFilter(new[] { ScoreRank.XH }); + notFoundPlaceholderShown(); + + setRankAchievedFilter(Array.Empty()); + notFoundPlaceholderShown(); + + // only Played filter + setPlayedFilter(SearchPlayed.Played); + notFoundPlaceholderShown(); + + setPlayedFilter(SearchPlayed.Any); + notFoundPlaceholderShown(); + + // both Rank Achieved and Played filters + setRankAchievedFilter(new[] { ScoreRank.XH }); + setPlayedFilter(SearchPlayed.Played); + notFoundPlaceholderShown(); + + setRankAchievedFilter(Array.Empty()); + setPlayedFilter(SearchPlayed.Any); + notFoundPlaceholderShown(); + } + + [Test] + public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithResults() + { + AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); + AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); + + // only Rank Achieved filter + setRankAchievedFilter(new[] { ScoreRank.XH }); + supporterRequiredPlaceholderShown(); + + setRankAchievedFilter(Array.Empty()); + noPlaceholderShown(); + + // only Played filter + setPlayedFilter(SearchPlayed.Played); + supporterRequiredPlaceholderShown(); + + setPlayedFilter(SearchPlayed.Any); + noPlaceholderShown(); + + // both Rank Achieved and Played filters + setRankAchievedFilter(new[] { ScoreRank.XH }); + setPlayedFilter(SearchPlayed.Played); + supporterRequiredPlaceholderShown(); + + setRankAchievedFilter(Array.Empty()); + setPlayedFilter(SearchPlayed.Any); + noPlaceholderShown(); + } + + [Test] + public void TestUserWithSupporterUsesSupporterOnlyFiltersWithResults() + { + AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); + AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); + + // only Rank Achieved filter + setRankAchievedFilter(new[] { ScoreRank.XH }); + noPlaceholderShown(); + + setRankAchievedFilter(Array.Empty()); + noPlaceholderShown(); + + // only Played filter + setPlayedFilter(SearchPlayed.Played); + noPlaceholderShown(); + + setPlayedFilter(SearchPlayed.Any); + noPlaceholderShown(); + + // both Rank Achieved and Played filters + setRankAchievedFilter(new[] { ScoreRank.XH }); + setPlayedFilter(SearchPlayed.Played); + noPlaceholderShown(); + + setRankAchievedFilter(Array.Empty()); + setPlayedFilter(SearchPlayed.Any); + noPlaceholderShown(); + } + private void fetchFor(params BeatmapSetInfo[] beatmaps) { setsForResponse.Clear(); setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b))); // trigger arbitrary change for fetching. - overlay.ChildrenOfType().Single().Query.TriggerChange(); + searchControl.Query.TriggerChange(); + } + + private void setRankAchievedFilter(ScoreRank[] ranks) + { + AddStep($"set Rank Achieved filter to [{string.Join(',', ranks)}]", () => + { + searchControl.Ranks.Clear(); + searchControl.Ranks.AddRange(ranks); + }); + } + + private void setPlayedFilter(SearchPlayed played) + { + AddStep($"set Played filter to {played}", () => searchControl.Played.Value = played); + } + + private void supporterRequiredPlaceholderShown() + { + AddUntilStep("\"supporter required\" placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + } + + private void notFoundPlaceholderShown() + { + AddUntilStep("\"no maps found\" placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + } + + private void noPlaceholderShown() + { + AddUntilStep("no placeholder shown", () => + !overlay.ChildrenOfType().Any() + && !overlay.ChildrenOfType().Any()); } private class TestAPIBeatmapSet : APIBeatmapSet diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 3971146ff8..a1549dfbce 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -82,7 +82,8 @@ namespace osu.Game.Tests.Visual.Online { switch (req) { - case JoinChannelRequest _: + case JoinChannelRequest joinChannel: + joinChannel.TriggerSuccess(); return true; } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 618447eae2..b16b61c5c7 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -3,25 +3,21 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.Containers; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Playlists; -using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Playlists { - public class TestScenePlaylistsLoungeSubScreen : RoomManagerTestScene + public class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene { - private LoungeSubScreen loungeScreen; + protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager; - [BackgroundDependencyLoader] - private void load() - { - } + private LoungeSubScreen loungeScreen; public override void SetUpSteps() { @@ -37,7 +33,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestScrollSelectedIntoView() { - AddRooms(30); + AddStep("add rooms", () => RoomManager.AddRooms(30)); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms.First())); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 44a79b6598..a320cb240f 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -3,7 +3,6 @@ using System; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,26 +11,28 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Playlists { - public class TestScenePlaylistsMatchSettingsOverlay : RoomTestScene + public class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene { - [Cached(Type = typeof(IRoomManager))] - private TestRoomManager roomManager = new TestRoomManager(); + protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; private TestRoomSettings settings; + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); + [SetUp] public new void Setup() => Schedule(() => { - settings = new TestRoomSettings + SelectedRoom.Value = new Room(); + + Child = settings = new TestRoomSettings { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible } }; - - Child = settings; }); [Test] @@ -39,19 +40,19 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("clear name and beatmap", () => { - Room.Name.Value = ""; - Room.Playlist.Clear(); + SelectedRoom.Value.Name.Value = ""; + SelectedRoom.Value.Playlist.Clear(); }); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set name", () => Room.Name.Value = "Room name"); + AddStep("set name", () => SelectedRoom.Value.Name.Value = "Room name"); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } })); + AddStep("set beatmap", () => SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } })); AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value); - AddStep("clear name", () => Room.Name.Value = ""); + AddStep("clear name", () => SelectedRoom.Value.Name.Value = ""); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); } @@ -67,9 +68,9 @@ namespace osu.Game.Tests.Visual.Playlists { settings.NameField.Current.Value = expected_name; settings.DurationField.Current.Value = expectedDuration; - Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); - roomManager.CreateRequested = r => + RoomManager.CreateRequested = r => { createdRoom = r; return true; @@ -88,11 +89,11 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("setup", () => { - Room.Name.Value = "Test Room"; - Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); + SelectedRoom.Value.Name.Value = "Test Room"; + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); fail = true; - roomManager.CreateRequested = _ => !fail; + RoomManager.CreateRequested = _ => !fail; }); AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); @@ -119,7 +120,12 @@ namespace osu.Game.Tests.Visual.Playlists public OsuSpriteText ErrorText => ((MatchSettings)Settings).ErrorText; } - private class TestRoomManager : IRoomManager + private class TestDependencies : OnlinePlayTestSceneDependencies + { + protected override IRoomManager CreateRoomManager() => new TestRoomManager(); + } + + protected class TestRoomManager : IRoomManager { public const string FAILED_TEXT = "failed"; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index 255f147ec9..76a78c0a3c 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -3,21 +3,23 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Users; namespace osu.Game.Tests.Visual.Playlists { - public class TestScenePlaylistsParticipantsList : RoomTestScene + public class TestScenePlaylistsParticipantsList : OnlinePlayTestScene { [SetUp] public new void Setup() => Schedule(() => { - Room.RoomID.Value = 7; + SelectedRoom.Value = new Room { RoomID = { Value = 7 } }; for (int i = 0; i < 50; i++) { - Room.RecentParticipants.Add(new User + SelectedRoom.Value.RecentParticipants.Add(new User { Username = "peppy", Statistics = new UserStatistics { GlobalRank = 1234 }, diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index a08a91314b..f2bfb80beb 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -15,20 +15,17 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Users; using osuTK.Input; namespace osu.Game.Tests.Visual.Playlists { - public class TestScenePlaylistsRoomSubScreen : RoomTestScene + public class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene { - [Cached(typeof(IRoomManager))] - private readonly TestRoomManager roomManager = new TestRoomManager(); - private BeatmapManager manager; private RulesetStore rulesets; @@ -40,8 +37,6 @@ namespace osu.Game.Tests.Visual.Playlists Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait(); - ((DummyAPIAccess)API).HandleRequest = req => { switch (req) @@ -58,7 +53,9 @@ namespace osu.Game.Tests.Visual.Playlists [SetUpSteps] public void SetupSteps() { - AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(Room))); + AddStep("set room", () => SelectedRoom.Value = new Room()); + AddStep("ensure has beatmap", () => manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait()); + AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value))); AddUntilStep("wait for load", () => match.IsCurrentScreen()); } @@ -67,12 +64,12 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("set room properties", () => { - Room.RoomID.Value = 1; - Room.Name.Value = "my awesome room"; - Room.Host.Value = new User { Id = 2, Username = "peppy" }; - Room.RecentParticipants.Add(Room.Host.Value); - Room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.RoomID.Value = 1; + SelectedRoom.Value.Name.Value = "my awesome room"; + SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" }; + SelectedRoom.Value.RecentParticipants.Add(SelectedRoom.Value.Host.Value); + SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo } @@ -88,9 +85,9 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("set room properties", () => { - Room.Name.Value = "my awesome room"; - Room.Host.Value = new User { Id = 2, Username = "peppy" }; - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Name.Value = "my awesome room"; + SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" }; + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo } @@ -104,17 +101,34 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("click", () => InputManager.Click(MouseButton.Left)); - AddAssert("first playlist item selected", () => match.SelectedItem.Value == Room.Playlist[0]); + AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]); } [Test] public void TestBeatmapUpdatedOnReImport() { BeatmapSetInfo importedSet = null; + TestBeatmap beatmap = null; + + // this step is required to make sure the further imports actually get online IDs. + // all the playlist logic relies on online ID matching. + AddStep("remove all matching online IDs", () => + { + beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo); + + var existing = manager.QueryBeatmapSets(s => s.OnlineBeatmapSetID == beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID).ToList(); + + foreach (var s in existing) + { + s.OnlineBeatmapSetID = null; + foreach (var b in s.Beatmaps) + b.OnlineBeatmapID = null; + manager.Update(s); + } + }); AddStep("import altered beatmap", () => { - var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo); beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1; importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result; @@ -122,9 +136,9 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("load room", () => { - Room.Name.Value = "my awesome room"; - Room.Host.Value = new User { Id = 2, Username = "peppy" }; - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Name.Value = "my awesome room"; + SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" }; + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedSet.Beatmaps[0] }, Ruleset = { Value = new OsuRuleset().RulesetInfo } @@ -155,30 +169,5 @@ namespace osu.Game.Tests.Visual.Playlists { } } - - private class TestRoomManager : IRoomManager - { - public event Action RoomsUpdated - { - add => throw new NotImplementedException(); - remove => throw new NotImplementedException(); - } - - public IBindable InitialRoomsReceived { get; } = new Bindable(true); - - public IBindableList Rooms { get; } = new BindableList(); - - public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) - { - room.RoomID.Value = 1; - onSuccess?.Invoke(room); - } - - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => onSuccess?.Invoke(room); - - public void PartRoom() - { - } - } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs index c5374d50ab..096bccae9e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; @@ -59,7 +60,7 @@ namespace osu.Game.Tests.Visual.UserInterface private class Icon : Container, IHasTooltip { - public string TooltipText { get; } + public LocalisableString TooltipText { get; } public SpriteIcon SpriteIcon { get; } diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 46c3b8bc3b..61f8511e3c 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -149,7 +149,8 @@ namespace osu.Game.Tournament.Tests.NonVisual private TournamentGameBase loadOsu(GameHost host) { var osu = new TournamentGameBase(); - Task.Run(() => host.Run(osu)); + Task.Run(() => host.Run(osu)) + .ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted); waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); return osu; } diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs index 4c5f5a7a1a..e4eb5a36fb 100644 --- a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs @@ -55,7 +55,8 @@ namespace osu.Game.Tournament.Tests.NonVisual private TournamentGameBase loadOsu(GameHost host) { var osu = new TournamentGameBase(); - Task.Run(() => host.Run(osu)); + Task.Run(() => host.Run(osu)) + .ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted); waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); return osu; } diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 069ddfa4db..27ad6650d1 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors [Resolved] protected IAPIProvider API { get; private set; } - private readonly Bindable beatmapId = new Bindable(); + private readonly Bindable beatmapId = new Bindable(); private readonly Bindable mods = new Bindable(); @@ -220,14 +220,12 @@ namespace osu.Game.Tournament.Screens.Editors [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - beatmapId.Value = Model.ID.ToString(); - beatmapId.BindValueChanged(idString => + beatmapId.Value = Model.ID; + beatmapId.BindValueChanged(id => { - int.TryParse(idString.NewValue, out var parsed); + Model.ID = id.NewValue ?? 0; - Model.ID = parsed; - - if (idString.NewValue != idString.OldValue) + if (id.NewValue != id.OldValue) Model.BeatmapInfo = null; if (Model.BeatmapInfo != null) diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 7bd8d3f6a0..6418bf97da 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors [Resolved] protected IAPIProvider API { get; private set; } - private readonly Bindable beatmapId = new Bindable(); + private readonly Bindable beatmapId = new Bindable(); private readonly Bindable score = new Bindable(); @@ -228,16 +228,12 @@ namespace osu.Game.Tournament.Screens.Editors [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - beatmapId.Value = Model.ID.ToString(); - beatmapId.BindValueChanged(idString => + beatmapId.Value = Model.ID; + beatmapId.BindValueChanged(id => { - int parsed; + Model.ID = id.NewValue ?? 0; - int.TryParse(idString.NewValue, out parsed); - - Model.ID = parsed; - - if (idString.NewValue != idString.OldValue) + if (id.NewValue != id.OldValue) Model.BeatmapInfo = null; if (Model.BeatmapInfo != null) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index aa1be143ea..0d2e64f300 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -214,7 +214,7 @@ namespace osu.Game.Tournament.Screens.Editors [Resolved] private TournamentGameBase game { get; set; } - private readonly Bindable userId = new Bindable(); + private readonly Bindable userId = new Bindable(); private readonly Container drawableContainer; @@ -278,14 +278,12 @@ namespace osu.Game.Tournament.Screens.Editors [BackgroundDependencyLoader] private void load() { - userId.Value = user.Id.ToString(); - userId.BindValueChanged(idString => + userId.Value = user.Id; + userId.BindValueChanged(id => { - int.TryParse(idString.NewValue, out var parsed); + user.Id = id.NewValue ?? 0; - user.Id = parsed; - - if (idString.NewValue != idString.OldValue) + if (id.NewValue != id.OldValue) user.Username = string.Empty; if (!string.IsNullOrEmpty(user.Username)) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 00af06703d..14bddb6319 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -181,8 +181,13 @@ namespace osu.Game.Beatmaps if (existingOnlineId != null) { Delete(existingOnlineId); - beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID); - LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged."); + + // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. + existingOnlineId.OnlineBeatmapSetID = null; + foreach (var b in existingOnlineId.Beatmaps) + b.OnlineBeatmapID = null; + + LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted."); } } } @@ -191,8 +196,6 @@ namespace osu.Game.Beatmaps { var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); - LogForModel(beatmapSet, $"Validating online IDs for {beatmapSet.Beatmaps.Count} beatmaps..."); - // ensure all IDs are unique if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) { @@ -319,6 +322,14 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); + protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import) + { + if (!base.CanReuseExisting(existing, import)) + return false; + + return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null); + } + protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) { if (!base.CanReuseExisting(existing, import)) diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index 5dff4fe282..7824205257 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -48,7 +48,6 @@ namespace osu.Game.Beatmaps public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) { - LogForModel(beatmapSet, "Performing online lookups..."); return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray()); } diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 55636495df..f373e59417 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -31,7 +31,7 @@ namespace osu.Game.Configuration { public LocalisableString Label { get; } - public string Description { get; } + public LocalisableString Description { get; } public int? OrderPosition { get; } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 8efd451857..f72a43fa01 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -78,7 +78,7 @@ namespace osu.Game.Database private readonly Bindable> itemRemoved = new Bindable>(); - public virtual IEnumerable HandledExtensions => new[] { ".zip" }; + public virtual IEnumerable HandledExtensions => new[] { @".zip" }; protected readonly FileStore Files; @@ -99,7 +99,7 @@ namespace osu.Game.Database ModelStore.ItemUpdated += item => handleEvent(() => itemUpdated.Value = new WeakReference(item)); ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference(item)); - exportStorage = storage.GetStorageForDirectory("exports"); + exportStorage = storage.GetStorageForDirectory(@"exports"); Files = new FileStore(contextFactory, storage); @@ -282,7 +282,7 @@ namespace osu.Game.Database } catch (Exception e) { - LogForModel(model, $"Model creation of {archive.Name} failed.", e); + LogForModel(model, @$"Model creation of {archive.Name} failed.", e); return null; } @@ -309,6 +309,12 @@ namespace osu.Game.Database Logger.Log($"{prefix} {message}", LoggingTarget.Database); } + /// + /// Whether the implementation overrides with a custom implementation. + /// Custom hash implementations must bypass the early exit in the import flow (see usage). + /// + protected virtual bool HasCustomHashFunction => false; + /// /// Create a SHA-2 hash from the provided archive based on file content of all files matching . /// @@ -317,7 +323,11 @@ namespace osu.Game.Database /// protected virtual string ComputeHash(TModel item, ArchiveReader reader = null) { - // for now, concatenate all .osu files in the set to create a unique hash. + if (reader != null) + // fast hashing for cases where the item's files may not be populated. + return computeHashFast(reader); + + // for now, concatenate all hashable files in the set to create a unique hash. MemoryStream hashable = new MemoryStream(); foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename)) @@ -329,9 +339,6 @@ namespace osu.Game.Database if (hashable.Length > 0) return hashable.ComputeSHA2Hash(); - if (reader != null) - return reader.Name.ComputeSHA2Hash(); - return item.Hash; } @@ -348,19 +355,48 @@ namespace osu.Game.Database delayEvents(); + bool checkedExisting = false; + TModel existing = null; + + if (archive != null && !HasCustomHashFunction) + { + // this is a fast bail condition to improve large import performance. + item.Hash = computeHashFast(archive); + + checkedExisting = true; + existing = CheckForExisting(item); + + if (existing != null) + { + // bare minimum comparisons + // + // note that this should really be checking filesizes on disk (of existing files) for some degree of sanity. + // or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files. + if (CanSkipImport(existing, item) && + getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f))) + { + LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); + Undelete(existing); + return existing; + } + + LogForModel(item, @"Found existing (optimised) but failed pre-check."); + } + } + void rollback() { if (!Delete(item)) { // We may have not yet added the model to the underlying table, but should still clean up files. - LogForModel(item, "Dereferencing files for incomplete import."); + LogForModel(item, @"Dereferencing files for incomplete import."); Files.Dereference(item.Files.Select(f => f.FileInfo).ToArray()); } } try { - LogForModel(item, "Beginning import..."); + LogForModel(item, @"Beginning import..."); item.Files = archive != null ? createFileInfos(archive, Files) : new List(); item.Hash = ComputeHash(item, archive); @@ -371,22 +407,24 @@ namespace osu.Game.Database { try { - if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}"); + if (!write.IsTransactionLeader) throw new InvalidOperationException(@$"Ensure there is no parent transaction so errors can correctly be handled by {this}"); - var existing = CheckForExisting(item); + if (!checkedExisting) + existing = CheckForExisting(item); if (existing != null) { if (CanReuseExisting(existing, item)) { Undelete(existing); - LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); + LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); // existing item will be used; rollback new import and exit early. rollback(); flushEvents(true); return existing; } + LogForModel(item, @"Found existing but failed re-use check."); Delete(existing); ModelStore.PurgeDeletable(s => s.ID == existing.ID); } @@ -403,12 +441,12 @@ namespace osu.Game.Database } } - LogForModel(item, "Import successfully completed!"); + LogForModel(item, @"Import successfully completed!"); } catch (Exception e) { if (!(e is TaskCanceledException)) - LogForModel(item, "Database import or population failed and has been rolled back.", e); + LogForModel(item, @"Database import or population failed and has been rolled back.", e); rollback(); flushEvents(false); @@ -428,7 +466,7 @@ namespace osu.Game.Database var retrievedItem = ModelStore.ConsumableItems.FirstOrDefault(s => s.ID == item.ID); if (retrievedItem == null) - throw new ArgumentException("Specified model could not be found", nameof(item)); + throw new ArgumentException(@"Specified model could not be found", nameof(item)); using (var outputStream = exportStorage.GetStream($"{getValidFilename(item.ToString())}{HandledExtensions.First()}", FileAccess.Write, FileMode.Create)) ExportModelTo(retrievedItem, outputStream); @@ -637,6 +675,22 @@ namespace osu.Game.Database } } + private string computeHashFast(ArchiveReader reader) + { + MemoryStream hashable = new MemoryStream(); + + foreach (var file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f)) + { + using (Stream s = reader.GetStream(file)) + s.CopyTo(hashable); + } + + if (hashable.Length > 0) + return hashable.ComputeSHA2Hash(); + + return reader.Name.ComputeSHA2Hash(); + } + /// /// Create all required s for the provided archive, adding them to the global file store. /// @@ -644,18 +698,14 @@ namespace osu.Game.Database { var fileInfos = new List(); - string prefix = reader.Filenames.GetCommonPrefix(); - if (!(prefix.EndsWith('/') || prefix.EndsWith('\\'))) - prefix = string.Empty; - // import files to manager - foreach (string file in reader.Filenames) + foreach (var filenames in getShortenedFilenames(reader)) { - using (Stream s = reader.GetStream(file)) + using (Stream s = reader.GetStream(filenames.original)) { fileInfos.Add(new TFileModel { - Filename = file.Substring(prefix.Length).ToStandardisedPath(), + Filename = filenames.shortened, FileInfo = files.Add(s) }); } @@ -664,6 +714,17 @@ namespace osu.Game.Database return fileInfos; } + private IEnumerable<(string original, string shortened)> getShortenedFilenames(ArchiveReader reader) + { + string prefix = reader.Filenames.GetCommonPrefix(); + if (!(prefix.EndsWith('/') || prefix.EndsWith('\\'))) + prefix = string.Empty; + + // import files to manager + foreach (string file in reader.Filenames) + yield return (file, file.Substring(prefix.Length).ToStandardisedPath()); + } + #region osu-stable import /// @@ -696,7 +757,7 @@ namespace osu.Game.Database { string fullPath = storage.GetFullPath(ImportFromStablePath); - Logger.Log($"Folder \"{fullPath}\" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error); + Logger.Log(@$"Folder ""{fullPath}"" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error); return Task.CompletedTask; } @@ -727,7 +788,7 @@ namespace osu.Game.Database /// The model to populate. /// The archive to use as a reference for population. May be null. /// An optional cancellation token. - protected virtual Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default) => Task.CompletedTask; + protected abstract Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default); /// /// Perform any final actions before the import to database executes. @@ -744,6 +805,15 @@ namespace osu.Game.Database /// An existing model which matches the criteria to skip importing, else null. protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); + /// + /// Whether inport can be skipped after finding an existing import early in the process. + /// Only valid when is not overridden. + /// + /// The existing model. + /// The newly imported model. + /// Whether to skip this import completely. + protected virtual bool CanSkipImport(TModel existing, TModel import) => true; + /// /// After an existing is found during an import process, the default behaviour is to use/restore the existing /// item and skip the import. This method allows changing that behaviour. @@ -771,7 +841,7 @@ namespace osu.Game.Database private DbSet queryModel() => ContextFactory.Get().Set(); - protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}"; + protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; #region Event handling / delaying diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index ed5931dd2b..fb5e2faff8 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -26,6 +26,11 @@ namespace osu.Game.Database /// private readonly object writeLock = new object(); + /// + /// Lock object which is held during sections. + /// + private readonly SemaphoreSlim blockingLock = new SemaphoreSlim(1); + private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); @@ -33,17 +38,12 @@ namespace osu.Game.Database private static readonly GlobalStatistic pending_writes = GlobalStatistics.Get("Realm", "Pending writes"); private static readonly GlobalStatistic active_usages = GlobalStatistics.Get("Realm", "Active usages"); - private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true); - private Realm context; public Realm Context { get { - if (IsDisposed) - throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory"); - if (context == null) { context = createContext(); @@ -64,7 +64,7 @@ namespace osu.Game.Database public RealmUsage GetForRead() { reads.Value++; - return new RealmUsage(this); + return new RealmUsage(createContext()); } public RealmWriteUsage GetForWrite() @@ -73,8 +73,34 @@ namespace osu.Game.Database pending_writes.Value++; Monitor.Enter(writeLock); + return new RealmWriteUsage(createContext(), writeComplete); + } - return new RealmWriteUsage(this); + /// + /// Flush any active contexts and block any further writes. + /// + /// + /// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm. + /// ie. to move the realm backing file to a new location. + /// + /// An which should be disposed to end the blocking section. + public IDisposable BlockAllOperations() + { + if (IsDisposed) + throw new ObjectDisposedException(nameof(RealmContextFactory)); + + Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); + + blockingLock.Wait(); + flushContexts(); + + return new InvokeOnDisposal(this, endBlockingSection); + + static void endBlockingSection(RealmContextFactory factory) + { + factory.blockingLock.Release(); + Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); + } } protected override void Update() @@ -87,15 +113,31 @@ namespace osu.Game.Database private Realm createContext() { - blockingResetEvent.Wait(); - - contexts_created.Value++; - - return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) + try { - SchemaVersion = schema_version, - MigrationCallback = onMigration, - }); + if (IsDisposed) + throw new ObjectDisposedException(nameof(RealmContextFactory)); + + blockingLock.Wait(); + + contexts_created.Value++; + + return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) + { + SchemaVersion = schema_version, + MigrationCallback = onMigration, + }); + } + finally + { + blockingLock.Release(); + } + } + + private void writeComplete() + { + Monitor.Exit(writeLock); + pending_writes.Value--; } private void onMigration(Migration migration, ulong lastSchemaVersion) @@ -109,28 +151,10 @@ namespace osu.Game.Database } } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - BlockAllOperations(); - } - - public IDisposable BlockAllOperations() - { - blockingResetEvent.Reset(); - flushContexts(); - - return new InvokeOnDisposal(this, r => endBlockingSection()); - } - - private void endBlockingSection() - { - blockingResetEvent.Set(); - } - private void flushContexts() { + Logger.Log(@"Flushing realm contexts...", LoggingTarget.Database); + var previousContext = context; context = null; @@ -139,6 +163,20 @@ namespace osu.Game.Database Thread.Sleep(50); previousContext?.Dispose(); + + Logger.Log(@"Realm contexts flushed.", LoggingTarget.Database); + } + + protected override void Dispose(bool isDisposing) + { + if (!IsDisposed) + { + // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. + BlockAllOperations(); + blockingLock?.Dispose(); + } + + base.Dispose(isDisposing); } /// @@ -148,13 +186,10 @@ namespace osu.Game.Database { public readonly Realm Realm; - protected readonly RealmContextFactory Factory; - - internal RealmUsage(RealmContextFactory factory) + internal RealmUsage(Realm context) { active_usages.Value++; - Factory = factory; - Realm = factory.createContext(); + Realm = context; } /// @@ -172,11 +207,13 @@ namespace osu.Game.Database /// public class RealmWriteUsage : RealmUsage { + private readonly Action onWriteComplete; private readonly Transaction transaction; - internal RealmWriteUsage(RealmContextFactory factory) - : base(factory) + internal RealmWriteUsage(Realm context, Action onWriteComplete) + : base(context) { + this.onWriteComplete = onWriteComplete; transaction = Realm.BeginWrite(); } @@ -200,8 +237,7 @@ namespace osu.Game.Database base.Dispose(); - Monitor.Exit(Factory.writeLock); - pending_writes.Value--; + onWriteComplete(); } } } diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs index 75c73af0ce..ce8a9c8f9f 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs @@ -4,12 +4,13 @@ using Markdig.Syntax.Inlines; using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; namespace osu.Game.Graphics.Containers.Markdown { public class OsuMarkdownImage : MarkdownImage, IHasTooltip { - public string TooltipText { get; } + public LocalisableString TooltipText { get; } public OsuMarkdownImage(LinkInline linkInline) : base(linkInline.Url) diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs index 60ded8952d..0bc3c876e1 100644 --- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs +++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.Containers @@ -24,7 +25,7 @@ namespace osu.Game.Graphics.Containers this.sampleSet = sampleSet; } - public virtual string TooltipText { get; set; } + public virtual LocalisableString TooltipText { get; set; } [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index 57f39bb8c7..81dca99ddd 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.Cursor @@ -32,7 +33,7 @@ namespace osu.Game.Graphics.Cursor public override bool SetContent(object content) { - if (!(content is string contentString)) + if (!(content is LocalisableString contentString)) return false; if (contentString == text.Text) return true; diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs index 5a1eb53fe1..6ad88eaaba 100644 --- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs +++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Platform; using osuTK; using osuTK.Graphics; @@ -58,6 +59,6 @@ namespace osu.Game.Graphics.UserInterface return true; } - public string TooltipText => "view in browser"; + public LocalisableString TooltipText => "view in browser"; } } diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index ac6f5ceb1b..8e82f4a7c1 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Platform; namespace osu.Game.Graphics.UserInterface @@ -105,7 +106,7 @@ namespace osu.Game.Graphics.UserInterface private class CapsWarning : SpriteIcon, IHasTooltip { - public string TooltipText => @"caps lock is active"; + public LocalisableString TooltipText => "caps lock is active"; public CapsWarning() { diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index f58962f8e1..ae16169123 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -34,7 +35,7 @@ namespace osu.Game.Graphics.UserInterface private readonly Box rightBox; private readonly Container nubContainer; - public virtual string TooltipText { get; private set; } + public virtual LocalisableString TooltipText { get; private set; } /// /// Whether to format the tooltip as a percentage or the actual value. diff --git a/osu.Game/Online/API/Requests/PostMessageRequest.cs b/osu.Game/Online/API/Requests/PostMessageRequest.cs index 84ab873acf..5d508a4cdf 100644 --- a/osu.Game/Online/API/Requests/PostMessageRequest.cs +++ b/osu.Game/Online/API/Requests/PostMessageRequest.cs @@ -9,11 +9,11 @@ namespace osu.Game.Online.API.Requests { public class PostMessageRequest : APIRequest { - private readonly Message message; + public readonly Message Message; public PostMessageRequest(Message message) { - this.message = message; + Message = message; } protected override WebRequest CreateWebRequest() @@ -21,12 +21,12 @@ namespace osu.Game.Online.API.Requests var req = base.CreateWebRequest(); req.Method = HttpMethod.Post; - req.AddParameter(@"is_action", message.IsAction.ToString().ToLowerInvariant()); - req.AddParameter(@"message", message.Content); + req.AddParameter(@"is_action", Message.IsAction.ToString().ToLowerInvariant()); + req.AddParameter(@"message", Message.Content); return req; } - protected override string Target => $@"chat/channels/{message.ChannelId}/messages"; + protected override string Target => $@"chat/channels/{Message.ChannelId}/messages"; } } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 8507887357..3136a3960d 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -225,7 +225,7 @@ namespace osu.Game.Online.Chat switch (command) { case "np": - AddInternal(new NowPlayingCommand()); + AddInternal(new NowPlayingCommand(target)); break; case "me": @@ -235,7 +235,7 @@ namespace osu.Game.Online.Chat break; } - PostMessage(content, true); + PostMessage(content, true, target); break; case "join": diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index 926709694b..7756591e03 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -21,6 +21,17 @@ namespace osu.Game.Online.Chat [Resolved] private Bindable currentBeatmap { get; set; } + private readonly Channel target; + + /// + /// Creates a new to post the currently-playing beatmap to a parenting . + /// + /// The target channel to post to. If null, the currently-selected channel will be posted to. + public NowPlayingCommand(Channel target = null) + { + this.target = target; + } + protected override void LoadComplete() { base.LoadComplete(); @@ -48,7 +59,7 @@ namespace osu.Game.Online.Chat var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString(); - channelManager.PostMessage($"is {verb} {beatmapString}", true); + channelManager.PostMessage($"is {verb} {beatmapString}", true, target); Expire(); } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 795540b65d..e35d3d6461 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -295,7 +296,7 @@ namespace osu.Game.Online.Leaderboards public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos); - public string TooltipText { get; } + public LocalisableString TooltipText { get; } public ScoreComponentLabel(LeaderboardScoreStatistic statistic) { @@ -365,7 +366,7 @@ namespace osu.Game.Online.Leaderboards }; } - public string TooltipText { get; } + public LocalisableString TooltipText { get; } } public class LeaderboardScoreStatistic diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 6195d8e1ea..7954eafdca 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -24,6 +24,7 @@ using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Logging; +using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Database; using osu.Game.Input; @@ -156,6 +157,8 @@ namespace osu.Game private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(GLOBAL_TRACK_VOLUME_ADJUST); + private IBindable updateThreadState; + public OsuGameBase() { UseDevelopmentServer = DebugUtils.IsDebugBuild; @@ -182,6 +185,10 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); + + updateThreadState = Host.UpdateThread.State.GetBoundCopy(); + updateThreadState.BindValueChanged(updateThreadStateChanged); + AddInternal(realmFactory); dependencies.CacheAs(Storage); @@ -356,6 +363,23 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); } + private IDisposable blocking; + + private void updateThreadStateChanged(ValueChangedEvent state) + { + switch (state.NewValue) + { + case GameThreadState.Running: + blocking?.Dispose(); + blocking = null; + break; + + case GameThreadState.Paused: + blocking = realmFactory.BlockAllOperations(); + break; + } + } + protected override void LoadComplete() { base.LoadComplete(); @@ -398,11 +422,15 @@ namespace osu.Game public void Migrate(string path) { + Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""..."); + using (realmFactory.BlockAllOperations()) { contextFactory.FlushConnections(); (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); } + + Logger.Log(@"Migration complete!"); } protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 1935a250b7..d80ef075e9 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -10,11 +10,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; +using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -23,9 +25,9 @@ namespace osu.Game.Overlays.BeatmapListing public class BeatmapListingFilterControl : CompositeDrawable { /// - /// Fired when a search finishes. Contains only new items in the case of pagination. + /// Fired when a search finishes. /// - public Action> SearchFinished; + public Action SearchFinished; /// /// Fired when search criteria change. @@ -212,7 +214,25 @@ namespace osu.Game.Overlays.BeatmapListing lastResponse = response; getSetsRequest = null; - SearchFinished?.Invoke(sets); + // check if a non-supporter used supporter-only filters + if (!api.LocalUser.Value.IsSupporter) + { + List filters = new List(); + + if (searchControl.Played.Value != SearchPlayed.Any) + filters.Add(BeatmapsStrings.ListingSearchFiltersPlayed); + + if (searchControl.Ranks.Any()) + filters.Add(BeatmapsStrings.ListingSearchFiltersRank); + + if (filters.Any()) + { + SearchFinished?.Invoke(SearchResult.SupporterOnlyFilters(filters)); + return; + } + } + + SearchFinished?.Invoke(SearchResult.ResultsReturned(sets)); }; api.Queue(getSetsRequest); @@ -237,5 +257,53 @@ namespace osu.Game.Overlays.BeatmapListing base.Dispose(isDisposing); } + + /// + /// Indicates the type of result of a user-requested beatmap search. + /// + public enum SearchResultType + { + /// + /// Actual results have been returned from API. + /// + ResultsReturned, + + /// + /// The user is not a supporter, but used supporter-only search filters. + /// + SupporterOnlyFilters + } + + /// + /// Describes the result of a user-requested beatmap search. + /// + public struct SearchResult + { + public SearchResultType Type { get; private set; } + + /// + /// Contains the beatmap sets returned from API. + /// Valid for read if and only if is . + /// + public List Results { get; private set; } + + /// + /// Contains the names of supporter-only filters requested by the user. + /// Valid for read if and only if is . + /// + public List SupporterOnlyFiltersUsed { get; private set; } + + public static SearchResult ResultsReturned(List results) => new SearchResult + { + Type = SearchResultType.ResultsReturned, + Results = results + }; + + public static SearchResult SupporterOnlyFilters(List filters) => new SearchResult + { + Type = SearchResultType.SupporterOnlyFilters, + SupporterOnlyFiltersUsed = filters + }; + } } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 5e65cd9488..460b4ba4c9 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Localisation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -15,7 +16,9 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.Containers; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Resources.Localisation.Web; @@ -33,6 +36,7 @@ namespace osu.Game.Overlays private Container panelTarget; private FillFlowContainer foundContent; private NotFoundDrawable notFoundContent; + private SupporterRequiredDrawable supporterRequiredContent; private BeatmapListingFilterControl filterControl; public BeatmapListingOverlay() @@ -76,6 +80,7 @@ namespace osu.Game.Overlays { foundContent = new FillFlowContainer(), notFoundContent = new NotFoundDrawable(), + supporterRequiredContent = new SupporterRequiredDrawable(), } } }, @@ -115,9 +120,16 @@ namespace osu.Game.Overlays private Task panelLoadDelegate; - private void onSearchFinished(List beatmaps) + private void onSearchFinished(BeatmapListingFilterControl.SearchResult searchResult) { - var newPanels = beatmaps.Select(b => new GridBeatmapPanel(b) + if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters) + { + supporterRequiredContent.UpdateText(searchResult.SupporterOnlyFiltersUsed); + addContentToPlaceholder(supporterRequiredContent); + return; + } + + var newPanels = searchResult.Results.Select(b => new GridBeatmapPanel(b) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -128,7 +140,7 @@ namespace osu.Game.Overlays //No matches case if (!newPanels.Any()) { - LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + addContentToPlaceholder(notFoundContent); return; } @@ -170,9 +182,9 @@ namespace osu.Game.Overlays { var transform = lastContent.FadeOut(100, Easing.OutQuint); - if (lastContent == notFoundContent) + if (lastContent == notFoundContent || lastContent == supporterRequiredContent) { - // not found display may be used multiple times, so don't expire/dispose it. + // the placeholders may be used multiple times, so don't expire/dispose them. transform.Schedule(() => panelTarget.Remove(lastContent)); } else @@ -240,6 +252,67 @@ namespace osu.Game.Overlays } } + // TODO: localisation requires Text/LinkFlowContainer support for localising strings with links inside + // (https://github.com/ppy/osu-framework/issues/4530) + public class SupporterRequiredDrawable : CompositeDrawable + { + private LinkFlowContainer supporterRequiredText; + + public SupporterRequiredDrawable() + { + RelativeSizeAxes = Axes.X; + Height = 225; + Alpha = 0; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + AddInternal(new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Texture = textures.Get(@"Online/supporter-required"), + }, + supporterRequiredText = new LinkFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Bottom = 10 }, + }, + } + }); + } + + public void UpdateText(List filters) + { + supporterRequiredText.Clear(); + + supporterRequiredText.AddText( + BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(), + t => + { + t.Font = OsuFont.GetFont(size: 16); + t.Colour = Colour4.White; + } + ); + + supporterRequiredText.AddLink(BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), @"/store/products/supporter-tag"); + } + } + private const double time_between_fetches = 500; private double lastFetchDisplayedTime; diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index cf74c0d4d3..b81c60a5b9 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.BeatmapSet { private readonly OsuSpriteText value; - public string TooltipText { get; } + public LocalisableString TooltipText { get; } public LocalisableString Value { diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs index 7ad6906cea..bb87e7151b 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -28,7 +29,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly IBindable localUser = new Bindable(); - public string TooltipText + public LocalisableString TooltipText { get { diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs index 6d27342049..cef623e59b 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -26,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly bool noVideo; - public string TooltipText => button.Enabled.Value ? "download this beatmap" : "login to download"; + public LocalisableString TooltipText => button.Enabled.Value ? "download this beatmap" : "login to download"; private readonly IBindable localUser = new Bindable(); diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 7c47ac655f..d94f8c4b8b 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -20,6 +20,7 @@ using System; using osu.Framework.Graphics.Shapes; using osu.Framework.Extensions.IEnumerableExtensions; using System.Collections.Specialized; +using osu.Framework.Localisation; using osu.Game.Overlays.Comments.Buttons; namespace osu.Game.Overlays.Comments @@ -395,7 +396,7 @@ namespace osu.Game.Overlays.Comments private class ParentUsername : FillFlowContainer, IHasTooltip { - public string TooltipText => getParentMessage(); + public LocalisableString TooltipText => getParentMessage(); private readonly Comment parentComment; @@ -427,7 +428,7 @@ namespace osu.Game.Overlays.Comments if (parentComment == null) return string.Empty; - return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? @"deleted" : string.Empty; + return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? "deleted" : string.Empty; } } } diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 70424101fd..d0bd24496a 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -14,6 +14,7 @@ using System; using System.Linq; using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; @@ -34,7 +35,7 @@ namespace osu.Game.Overlays.Mods /// public Action SelectionChanged; - public string TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty; + public LocalisableString TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty; private const Easing mod_switch_easing = Easing.InOutSine; private const double mod_switch_duration = 120; diff --git a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs index 87b9d89d4d..0ece96b56c 100644 --- a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs +++ b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics.UserInterface; using osu.Framework.Allocation; using osuTK.Graphics; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; namespace osu.Game.Overlays { @@ -56,7 +57,7 @@ namespace osu.Game.Overlays [Resolved] private OverlayColourProvider colourProvider { get; set; } - public string TooltipText => $@"{Value} view"; + public LocalisableString TooltipText => $@"{Value} view"; private readonly SpriteIcon icon; diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs index 7eed4d3b6b..74f3ed846b 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; using osu.Game.Users; using osuTK; @@ -42,6 +43,6 @@ namespace osu.Game.Overlays.Profile.Header.Components InternalChild.FadeInFromZero(200); } - public string TooltipText => badge.Description; + public LocalisableString TooltipText => badge.Description; } } diff --git a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs index 29e13e4f51..527c70685f 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osuTK; namespace osu.Game.Overlays.Profile.Header.Components @@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly BindableBool DetailsVisible = new BindableBool(); - public override string TooltipText => DetailsVisible.Value ? "collapse" : "expand"; + public override LocalisableString TooltipText => DetailsVisible.Value ? "collapse" : "expand"; private SpriteIcon icon; diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs index bd8aa7b3bd..db94762efd 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components @@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public override string TooltipText => "followers"; + public override LocalisableString TooltipText => "followers"; protected override IconUsage Icon => FontAwesome.Solid.User; diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs index 29471375b5..a0b8ef0f11 100644 --- a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs +++ b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Users; @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public string TooltipText { get; } + public LocalisableString TooltipText { get; } private OsuSpriteText levelText; diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs index c97df3bc4d..528b05a80a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs +++ b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public string TooltipText { get; } + public LocalisableString TooltipText { get; } private Bar levelProgressBar; private OsuSpriteText levelProgressText; diff --git a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs index b4d7c9a05c..ae3d024fbf 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components @@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public override string TooltipText => "mapping subscribers"; + public override LocalisableString TooltipText => "mapping subscribers"; protected override IconUsage Icon => FontAwesome.Solid.Bell; diff --git a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs index 228765ee1a..4c2cc768ce 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Users; @@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public override string TooltipText => "send message"; + public override LocalisableString TooltipText => "send message"; [Resolved(CanBeNull = true)] private ChannelManager channelManager { get; set; } diff --git a/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs b/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs index be96840217..aa7cb8636a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs +++ b/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components @@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public string TooltipText { get; set; } + public LocalisableString TooltipText { get; set; } private OverlinedInfoContainer info; diff --git a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs index d581e2750c..9a43997030 100644 --- a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs +++ b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Graphics; namespace osu.Game.Overlays.Profile.Header.Components @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private readonly FillFlowContainer iconContainer; private readonly CircularContainer content; - public string TooltipText => "osu!supporter"; + public LocalisableString TooltipText => "osu!supporter"; public int SupportLevel { diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index 6d6ff32aac..6f1869966a 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -143,7 +143,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private class PlayCountText : CompositeDrawable, IHasTooltip { - public string TooltipText => "times played"; + public LocalisableString TooltipText => "times played"; public PlayCountText(int playCount) { diff --git a/osu.Game/Overlays/RestoreDefaultValueButton.cs b/osu.Game/Overlays/RestoreDefaultValueButton.cs index 213ad2ba68..fe36f6ba6d 100644 --- a/osu.Game/Overlays/RestoreDefaultValueButton.cs +++ b/osu.Game/Overlays/RestoreDefaultValueButton.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; @@ -76,7 +77,7 @@ namespace osu.Game.Overlays UpdateState(); } - public string TooltipText => "revert to default"; + public LocalisableString TooltipText => "revert to default"; protected override bool OnHover(HoverEvent e) { diff --git a/osu.Game/Overlays/Settings/OutlinedTextBox.cs b/osu.Game/Overlays/Settings/OutlinedTextBox.cs new file mode 100644 index 0000000000..93eaf74b77 --- /dev/null +++ b/osu.Game/Overlays/Settings/OutlinedTextBox.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.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Settings +{ + public class OutlinedTextBox : OsuTextBox + { + private const float border_thickness = 3; + + private Color4 borderColourFocused; + private Color4 borderColourUnfocused; + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + borderColourUnfocused = colour.Gray4.Opacity(0.5f); + borderColourFocused = BorderColour; + + updateBorder(); + } + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + updateBorder(); + } + + protected override void OnFocusLost(FocusLostEvent e) + { + base.OnFocusLost(e); + + updateBorder(); + } + + private void updateBorder() + { + BorderThickness = border_thickness; + BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index c9a81b955b..1ae297f2a9 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -32,7 +33,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio private class OffsetSlider : OsuSliderBar { - public override string TooltipText => Current.Value.ToString(@"0ms"); + public override LocalisableString TooltipText => Current.Value.ToString(@"0ms"); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 937bcc8abf..669753d2cb 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -233,7 +233,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private class UIScaleSlider : OsuSliderBar { - public override string TooltipText => base.TooltipText + "x"; + public override LocalisableString TooltipText => base.TooltipText + "x"; } private class ResolutionSettingsDropdown : SettingsDropdown diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index fb908a7669..e87572e2ca 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Input.Handlers.Mouse; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -116,7 +117,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private class SensitivitySlider : OsuSliderBar { - public override string TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x"; + public override LocalisableString TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x"; } } } diff --git a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs index 101d8f43f7..8aeb440be1 100644 --- a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs +++ b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings.Sections @@ -10,6 +11,6 @@ namespace osu.Game.Overlays.Settings.Sections /// internal class SizeSlider : OsuSliderBar { - public override string TooltipText => Current.Value.ToString(@"0.##x"); + public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x"); } } diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 19adfc5dd9..a6eb008623 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -44,7 +45,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface private class TimeSlider : OsuSliderBar { - public override string TooltipText => Current.Value.ToString("N0") + "ms"; + public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms"; } } } diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index c73a783d37..2470c0a6c5 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -62,12 +63,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface private class MaximumStarsSlider : StarsSlider { - public override string TooltipText => Current.IsDefault ? "no limit" : base.TooltipText; + public override LocalisableString TooltipText => Current.IsDefault ? "no limit" : base.TooltipText; } private class StarsSlider : OsuSliderBar { - public override string TooltipText => Current.Value.ToString(@"0.## stars"); + public override LocalisableString TooltipText => Current.Value.ToString(@"0.## stars"); } } } diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs index 088d69c031..87b1aa0e46 100644 --- a/osu.Game/Overlays/Settings/SettingsButton.cs +++ b/osu.Game/Overlays/Settings/SettingsButton.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings @@ -17,14 +18,15 @@ namespace osu.Game.Overlays.Settings Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }; } - public string TooltipText { get; set; } + public LocalisableString TooltipText { get; set; } public override IEnumerable FilterTerms { get { - if (TooltipText != null) - return base.FilterTerms.Append(TooltipText); + if (TooltipText != default) + // TODO: this won't work as intended once the tooltip text is translated. + return base.FilterTerms.Append(TooltipText.ToString()); return base.FilterTerms; } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 807916e7f6..15a0a42d31 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings public bool ShowsDefaultIndicator = true; - public string TooltipText { get; set; } + public LocalisableString TooltipText { get; set; } [Resolved] private OsuColour colours { get; set; } @@ -142,4 +142,4 @@ namespace osu.Game.Overlays.Settings labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index ca9a8e9c08..2fbe522479 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -1,19 +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 osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { - public class SettingsNumberBox : SettingsItem + public class SettingsNumberBox : SettingsItem { - protected override Drawable CreateControl() => new NumberBox + protected override Drawable CreateControl() => new NumberControl { - Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 5 } }; - public class NumberBox : SettingsTextBox.TextBox + private sealed class NumberControl : CompositeDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + public NumberControl() + { + AutoSizeAxes = Axes.Y; + + OutlinedNumberBox numberBox; + + InternalChildren = new[] + { + numberBox = new OutlinedNumberBox + { + Margin = new MarginPadding { Top = 5 }, + RelativeSizeAxes = Axes.X, + CommitOnFocusLost = true + } + }; + + numberBox.Current.BindValueChanged(e => + { + int? value = null; + + if (int.TryParse(e.NewValue, out var intVal)) + value = intVal; + + current.Value = value; + }); + + Current.BindValueChanged(e => + { + numberBox.Current.Value = e.NewValue?.ToString(); + }); + } + } + + private class OutlinedNumberBox : OutlinedTextBox { protected override bool CanAddCharacter(char character) => char.IsNumber(character); } diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index 25424e85a1..d28dbf1068 100644 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs @@ -1,60 +1,17 @@ // 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.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osuTK.Graphics; namespace osu.Game.Overlays.Settings { public class SettingsTextBox : SettingsItem { - protected override Drawable CreateControl() => new TextBox + protected override Drawable CreateControl() => new OutlinedTextBox { Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true, + CommitOnFocusLost = true }; - - public class TextBox : OsuTextBox - { - private const float border_thickness = 3; - - private Color4 borderColourFocused; - private Color4 borderColourUnfocused; - - [BackgroundDependencyLoader] - private void load(OsuColour colour) - { - borderColourUnfocused = colour.Gray4.Opacity(0.5f); - borderColourFocused = BorderColour; - - updateBorder(); - } - - protected override void OnFocus(FocusEvent e) - { - base.OnFocus(e); - - updateBorder(); - } - - protected override void OnFocusLost(FocusLostEvent e) - { - base.OnFocusLost(e); - - updateBorder(); - } - - private void updateBorder() - { - BorderThickness = border_thickness; - BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused; - } - } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 432c52c2e9..4a33f9e296 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -159,28 +159,6 @@ namespace osu.Game.Overlays.Toolbar }; } - private RealmKeyBinding realmKeyBinding; - - protected override void LoadComplete() - { - base.LoadComplete(); - - if (Hotkey == null) return; - - realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); - - if (realmKeyBinding != null) - { - realmKeyBinding.PropertyChanged += (sender, args) => - { - if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString)) - updateKeyBindingTooltip(); - }; - } - - updateKeyBindingTooltip(); - } - protected override bool OnMouseDown(MouseDownEvent e) => true; protected override bool OnClick(ClickEvent e) @@ -196,6 +174,7 @@ namespace osu.Game.Overlays.Toolbar HoverBackground.FadeIn(200); tooltipContainer.FadeIn(100); + return base.OnHover(e); } @@ -222,6 +201,10 @@ namespace osu.Game.Overlays.Toolbar private void updateKeyBindingTooltip() { + if (Hotkey == null) return; + + var realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); + if (realmKeyBinding != null) { var keyBindingString = realmKeyBinding.KeyCombination.ReadableString(); diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index d208c7fe07..706eec226c 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Edit // Audio new CheckAudioPresence(), new CheckAudioQuality(), + new CheckMutedObjects(), + new CheckFewHitsounds(), // Compose new CheckUnsnappedObjects(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs new file mode 100644 index 0000000000..5185ba6c99 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs @@ -0,0 +1,164 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Audio; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckFewHitsounds : ICheck + { + /// + /// 2 measures (4/4) of 120 BPM, typically makes up a few patterns in the map. + /// This is almost always ok, but can still be useful for the mapper to make sure hitsounding coverage is good. + /// + private const int negligible_threshold_time = 4000; + + /// + /// 4 measures (4/4) of 120 BPM, typically makes up a large portion of a section in the song. + /// This is ok if the section is a quiet intro, for example. + /// + private const int warning_threshold_time = 8000; + + /// + /// 12 measures (4/4) of 120 BPM, typically makes up multiple sections in the song. + /// + private const int problem_threshold_time = 24000; + + // Should pass at least this many objects without hitsounds to be considered an issue (should work for Easy diffs too). + private const int warning_threshold_objects = 4; + private const int problem_threshold_objects = 16; + + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Few or no hitsounds"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateLongPeriodProblem(this), + new IssueTemplateLongPeriodWarning(this), + new IssueTemplateLongPeriodNegligible(this), + new IssueTemplateNoHitsounds(this) + }; + + private bool mapHasHitsounds; + private int objectsWithoutHitsounds; + private double lastHitsoundTime; + + public IEnumerable Run(BeatmapVerifierContext context) + { + if (!context.Beatmap.HitObjects.Any()) + yield break; + + mapHasHitsounds = false; + objectsWithoutHitsounds = 0; + lastHitsoundTime = context.Beatmap.HitObjects.First().StartTime; + + var hitObjectsIncludingNested = new List(); + + foreach (var hitObject in context.Beatmap.HitObjects) + { + // Samples play on the end of objects. Some objects have nested objects to accomplish playing them elsewhere (e.g. slider head/repeat). + foreach (var nestedHitObject in hitObject.NestedHitObjects) + hitObjectsIncludingNested.Add(nestedHitObject); + + hitObjectsIncludingNested.Add(hitObject); + } + + var hitObjectsByEndTime = hitObjectsIncludingNested.OrderBy(o => o.GetEndTime()).ToList(); + var hitObjectCount = hitObjectsByEndTime.Count; + + for (int i = 0; i < hitObjectCount; ++i) + { + var hitObject = hitObjectsByEndTime[i]; + + // This is used to perform an update at the end so that the period after the last hitsounded object can be an issue. + bool isLastObject = i == hitObjectCount - 1; + + foreach (var issue in applyHitsoundUpdate(hitObject, isLastObject)) + yield return issue; + } + + if (!mapHasHitsounds) + yield return new IssueTemplateNoHitsounds(this).Create(); + } + + private IEnumerable applyHitsoundUpdate(HitObject hitObject, bool isLastObject = false) + { + var time = hitObject.GetEndTime(); + bool hasHitsound = hitObject.Samples.Any(isHitsound); + bool couldHaveHitsound = hitObject.Samples.Any(isHitnormal); + + // Only generating issues on hitsounded or last objects ensures we get one issue per long period. + // If there are no hitsounds we let the "No hitsounds" template take precedence. + if (hasHitsound || (isLastObject && mapHasHitsounds)) + { + var timeWithoutHitsounds = time - lastHitsoundTime; + + if (timeWithoutHitsounds > problem_threshold_time && objectsWithoutHitsounds > problem_threshold_objects) + yield return new IssueTemplateLongPeriodProblem(this).Create(lastHitsoundTime, timeWithoutHitsounds); + else if (timeWithoutHitsounds > warning_threshold_time && objectsWithoutHitsounds > warning_threshold_objects) + yield return new IssueTemplateLongPeriodWarning(this).Create(lastHitsoundTime, timeWithoutHitsounds); + else if (timeWithoutHitsounds > negligible_threshold_time && objectsWithoutHitsounds > warning_threshold_objects) + yield return new IssueTemplateLongPeriodNegligible(this).Create(lastHitsoundTime, timeWithoutHitsounds); + } + + if (hasHitsound) + { + mapHasHitsounds = true; + objectsWithoutHitsounds = 0; + lastHitsoundTime = time; + } + else if (couldHaveHitsound) + ++objectsWithoutHitsounds; + } + + private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.AllAdditions.Any(sample.Name.Contains); + private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL); + + public abstract class IssueTemplateLongPeriod : IssueTemplate + { + protected IssueTemplateLongPeriod(ICheck check, IssueType type) + : base(check, type, "Long period without hitsounds ({0:F1} seconds).") + { + } + + public Issue Create(double time, double duration) => new Issue(this, duration / 1000f) { Time = time }; + } + + public class IssueTemplateLongPeriodProblem : IssueTemplateLongPeriod + { + public IssueTemplateLongPeriodProblem(ICheck check) + : base(check, IssueType.Problem) + { + } + } + + public class IssueTemplateLongPeriodWarning : IssueTemplateLongPeriod + { + public IssueTemplateLongPeriodWarning(ICheck check) + : base(check, IssueType.Warning) + { + } + } + + public class IssueTemplateLongPeriodNegligible : IssueTemplateLongPeriod + { + public IssueTemplateLongPeriodNegligible(ICheck check) + : base(check, IssueType.Negligible) + { + } + } + + public class IssueTemplateNoHitsounds : IssueTemplate + { + public IssueTemplateNoHitsounds(ICheck check) + : base(check, IssueType.Problem, "There are no hitsounds.") + { + } + + public Issue Create() => new Issue(this); + } + } +} diff --git a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs new file mode 100644 index 0000000000..a4ff921b7e --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs @@ -0,0 +1,158 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Utils; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckMutedObjects : ICheck + { + /// + /// Volume percentages lower than or equal to this are typically inaudible. + /// + private const int muted_threshold = 5; + + /// + /// Volume percentages lower than or equal to this can sometimes be inaudible depending on sample used and music volume. + /// + private const int low_volume_threshold = 20; + + private enum EdgeType + { + Head, + Repeat, + Tail, + None + } + + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Low volume hitobjects"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateMutedActive(this), + new IssueTemplateLowVolumeActive(this), + new IssueTemplateMutedPassive(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + foreach (var hitObject in context.Beatmap.HitObjects) + { + // Worth keeping in mind: The samples of an object always play at its end time. + // Objects like spinners have no sound at its start because of this, while hold notes have nested objects to accomplish this. + foreach (var nestedHitObject in hitObject.NestedHitObjects) + { + foreach (var issue in getVolumeIssues(hitObject, nestedHitObject)) + yield return issue; + } + + foreach (var issue in getVolumeIssues(hitObject)) + yield return issue; + } + } + + private IEnumerable getVolumeIssues(HitObject hitObject, HitObject sampledHitObject = null) + { + sampledHitObject ??= hitObject; + if (!sampledHitObject.Samples.Any()) + yield break; + + // Samples that allow themselves to be overridden by control points have a volume of 0. + int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume); + double samplePlayTime = sampledHitObject.GetEndTime(); + + EdgeType edgeType = getEdgeAtTime(hitObject, samplePlayTime); + // We only care about samples played on the edges of objects, not ones like spinnerspin or slidertick. + if (edgeType == EdgeType.None) + yield break; + + string postfix = hitObject is IHasDuration ? edgeType.ToString().ToLower() : null; + + if (maxVolume <= muted_threshold) + { + if (edgeType == EdgeType.Head) + yield return new IssueTemplateMutedActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix); + else + yield return new IssueTemplateMutedPassive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix); + } + else if (maxVolume <= low_volume_threshold && edgeType == EdgeType.Head) + { + yield return new IssueTemplateLowVolumeActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix); + } + } + + private EdgeType getEdgeAtTime(HitObject hitObject, double time) + { + if (Precision.AlmostEquals(time, hitObject.StartTime, 1f)) + return EdgeType.Head; + if (Precision.AlmostEquals(time, hitObject.GetEndTime(), 1f)) + return EdgeType.Tail; + + if (hitObject is IHasRepeats hasRepeats) + { + double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount(); + if (spanDuration <= 0) + // Prevents undefined behaviour in cases like where zero/negative-length sliders/hold notes exist. + return EdgeType.None; + + double spans = (time - hitObject.StartTime) / spanDuration; + double acceptableDifference = 1 / spanDuration; // 1 ms of acceptable difference, as with head/tail above. + + if (Precision.AlmostEquals(spans, Math.Ceiling(spans), acceptableDifference) || + Precision.AlmostEquals(spans, Math.Floor(spans), acceptableDifference)) + { + return EdgeType.Repeat; + } + } + + return EdgeType.None; + } + + public abstract class IssueTemplateMuted : IssueTemplate + { + protected IssueTemplateMuted(ICheck check, IssueType type, string unformattedMessage) + : base(check, type, unformattedMessage) + { + } + + public Issue Create(HitObject hitobject, double volume, double time, string postfix = "") + { + string objectName = hitobject.GetType().Name; + if (!string.IsNullOrEmpty(postfix)) + objectName += " " + postfix; + + return new Issue(hitobject, this, objectName, volume) { Time = time }; + } + } + + public class IssueTemplateMutedActive : IssueTemplateMuted + { + public IssueTemplateMutedActive(ICheck check) + : base(check, IssueType.Problem, "{0} has a volume of {1:0%}. Clickable objects must have clearly audible feedback.") + { + } + } + + public class IssueTemplateLowVolumeActive : IssueTemplateMuted + { + public IssueTemplateLowVolumeActive(ICheck check) + : base(check, IssueType.Warning, "{0} has a volume of {1:0%}, ensure this is audible.") + { + } + } + + public class IssueTemplateMutedPassive : IssueTemplateMuted + { + public IssueTemplateMutedPassive(ICheck check) + : base(check, IssueType.Negligible, "{0} has a volume of {1:0%}, ensure there is no distinct sound here in the song if inaudible.") + { + } + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index 61297c162d..1f7742b075 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods { @@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.Dice; public override double ScoreMultiplier => 1; - [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))] + [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))] public Bindable Seed { get; } = new Bindable { Default = null, diff --git a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs b/osu.Game/Rulesets/Mods/SeedSettingsControl.cs deleted file mode 100644 index 5c57717d93..0000000000 --- a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs +++ /dev/null @@ -1,92 +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.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.Settings; - -namespace osu.Game.Rulesets.Mods -{ - /// - /// A settings control for use by mods which have a customisable seed value. - /// - public class SeedSettingsControl : SettingsItem - { - protected override Drawable CreateControl() => new SeedControl - { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 5 } - }; - - private sealed class SeedControl : CompositeDrawable, IHasCurrentValue - { - private readonly BindableWithCurrent current = new BindableWithCurrent(); - - public Bindable Current - { - get => current; - set - { - current.Current = value; - seedNumberBox.Text = value.Value.ToString(); - } - } - - private readonly OsuNumberBox seedNumberBox; - - public SeedControl() - { - AutoSizeAxes = Axes.Y; - - InternalChildren = new[] - { - new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 2), - new Dimension(GridSizeMode.Relative, 0.25f) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - seedNumberBox = new OsuNumberBox - { - RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true - } - } - } - } - }; - - seedNumberBox.Current.BindValueChanged(e => - { - int? value = null; - - if (int.TryParse(e.NewValue, out var intVal)) - value = intVal; - - current.Value = value; - }); - } - - protected override void Update() - { - if (current.Value == null) - seedNumberBox.Text = current.Current.Value.ToString(); - } - } - } -} diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index cae5da3d16..725cfa9c26 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osuTK; using osu.Framework.Bindables; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.UI { @@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.UI private const float size = 80; - public virtual string TooltipText => showTooltip ? mod.IconTooltip : null; + public virtual LocalisableString TooltipText => showTooltip ? mod.IconTooltip : null; private Mod mod; private readonly bool showTooltip; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 3944c1d3de..4fd1d00fef 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -46,7 +46,7 @@ namespace osu.Game.Scoring [JsonIgnore] public int Combo { get; set; } // Todo: Shouldn't exist in here - [JsonIgnore] + [JsonProperty("ruleset_id")] public int RulesetID { get; set; } [JsonProperty("passed")] diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 9d3b952ada..d5bea0affc 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Linq.Expressions; using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework.Bindables; @@ -72,6 +73,9 @@ namespace osu.Game.Scoring } } + protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) + => Task.CompletedTask; + protected override void ExportModelTo(ScoreInfo model, Stream outputStream) { var file = model.Files.SingleOrDefault(); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs index 3b1dae6c3d..3ac40fda0f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osuTK; using osuTK.Graphics; @@ -58,6 +59,6 @@ namespace osu.Game.Screens.Edit.Compose.Components icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint); } - public string TooltipText { get; } + public LocalisableString TooltipText { get; } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs index c4dc2a2b8f..ae1ca1b967 100644 --- a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Online.Rooms; @@ -16,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { private readonly GameType type; - public string TooltipText => type.Name; + public LocalisableString TooltipText => type.Name; public DrawableGameType(GameType type) { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 8e59dc8579..5f135a3e90 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -93,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { bool matchingFilter = true; - matchingFilter &= r.Room.Playlist.Count == 0 || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset)); + matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset)); if (!string.IsNullOrEmpty(criteria.SearchString)) matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs index 9e1a020eca..20d12d62a3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs @@ -34,7 +34,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void Stop() => IsRunning = false; - public bool Seek(double position) => true; + public bool Seek(double position) + { + CurrentTime = position; + return true; + } public void ResetSpeedAdjustments() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs index efc12eaaa5..94278a47b6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Timing; @@ -28,16 +31,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public const double MAXIMUM_START_DELAY = 15000; + public event Action ReadyToStart; + /// /// The master clock which is used to control the timing of all player clocks clocks. /// public IAdjustableClock MasterClock { get; } + public IBindable MasterState => masterState; + /// /// The player clocks. /// private readonly List playerClocks = new List(); + private readonly Bindable masterState = new Bindable(); + private bool hasStarted; private double? firstStartAttemptTime; @@ -46,7 +55,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate MasterClock = master; } - public void AddPlayerClock(ISpectatorPlayerClock clock) => playerClocks.Add(clock); + public void AddPlayerClock(ISpectatorPlayerClock clock) + { + Debug.Assert(!playerClocks.Contains(clock)); + playerClocks.Add(clock); + } public void RemovePlayerClock(ISpectatorPlayerClock clock) => playerClocks.Remove(clock); @@ -62,8 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return; } - updateCatchup(); - updateMasterClock(); + updatePlayerCatchup(); + updateMasterState(); } /// @@ -81,14 +94,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value); if (readyCount == playerClocks.Count) - return hasStarted = true; + return performStart(); if (readyCount > 0) { firstStartAttemptTime ??= Time.Current; if (Time.Current - firstStartAttemptTime > MAXIMUM_START_DELAY) - return hasStarted = true; + return performStart(); + } + + bool performStart() + { + ReadyToStart?.Invoke(); + return hasStarted = true; } return false; @@ -97,7 +116,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// Updates the catchup states of all player clocks clocks. /// - private void updateCatchup() + private void updatePlayerCatchup() { for (int i = 0; i < playerClocks.Count; i++) { @@ -135,19 +154,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } /// - /// Updates the master clock's running state. + /// Updates the state of the master clock. /// - private void updateMasterClock() + private void updateMasterState() { bool anyInSync = playerClocks.Any(s => !s.IsCatchingUp); - - if (MasterClock.IsRunning != anyInSync) - { - if (anyInSync) - MasterClock.Start(); - else - MasterClock.Stop(); - } + masterState.Value = anyInSync ? MasterClockState.Synchronised : MasterClockState.TooFarAhead; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs index bd698108f6..3c644ccb78 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.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 osu.Framework.Bindables; using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate @@ -10,11 +12,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public interface ISyncManager { + /// + /// An event which is invoked when gameplay is ready to start. + /// + event Action ReadyToStart; + /// /// The master clock which player clocks should synchronise to. /// IAdjustableClock MasterClock { get; } + /// + /// An event which is invoked when the state of is changed. + /// + IBindable MasterState { get; } + /// /// Adds an to manage. /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MasterClockState.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MasterClockState.cs new file mode 100644 index 0000000000..8982d1669d --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MasterClockState.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public enum MasterClockState + { + /// + /// The master clock is synchronised with at least one player clock. + /// + Synchronised, + + /// + /// The master clock is too far ahead of any player clock and needs to slow down. + /// + TooFarAhead + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 983daac909..2a2759e0dd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; @@ -42,6 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private PlayerGrid grid; private MultiSpectatorLeaderboard leaderboard; private PlayerArea currentAudioSource; + private bool canStartMasterClock; /// /// Creates a new . @@ -100,15 +102,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Expanded = { Value = true }, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - }, leaderboardContainer.Add); + }, l => + { + foreach (var instance in instances) + leaderboard.AddClock(instance.UserId, instance.GameplayClock); + + leaderboardContainer.Add(leaderboard); + }); } protected override void LoadComplete() { base.LoadComplete(); - masterClockContainer.Stop(); masterClockContainer.Reset(); + masterClockContainer.Stop(); + + syncManager.ReadyToStart += onReadyToStart; + syncManager.MasterState.BindValueChanged(onMasterStateChanged, true); } protected override void Update() @@ -129,19 +140,45 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private bool isCandidateAudioSource([CanBeNull] ISpectatorPlayerClock clock) => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value; + private void onReadyToStart() + { + // Seek the master clock to the gameplay time. + // This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer. + var startTime = instances.Where(i => i.Score != null) + .SelectMany(i => i.Score.Replay.Frames) + .Select(f => f.Time) + .DefaultIfEmpty(0) + .Min(); + + masterClockContainer.Seek(startTime); + masterClockContainer.Start(); + + // Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it. + canStartMasterClock = true; + } + + private void onMasterStateChanged(ValueChangedEvent state) + { + switch (state.NewValue) + { + case MasterClockState.Synchronised: + if (canStartMasterClock) + masterClockContainer.Start(); + + break; + + case MasterClockState.TooFarAhead: + masterClockContainer.Stop(); + break; + } + } + protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) { } protected override void StartGameplay(int userId, GameplayState gameplayState) - { - var instance = instances.Single(i => i.UserId == userId); - - instance.LoadScore(gameplayState.Score); - - syncManager.AddPlayerClock(instance.GameplayClock); - leaderboard.AddClock(instance.UserId, instance.GameplayClock); - } + => instances.Single(i => i.UserId == userId).LoadScore(gameplayState.Score); protected override void EndGameplay(int userId) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index fe79e5db72..95ccc08608 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// Whether a is loaded in the area. /// - public bool PlayerLoaded => stack?.CurrentScreen is Player; + public bool PlayerLoaded => (stack?.CurrentScreen as Player)?.IsLoaded == true; /// /// The user id this corresponds to. diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 0286b6b8a6..f662a479ec 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -47,8 +47,9 @@ namespace osu.Game.Screens.Play { base.StartGameplay(); + // Start gameplay along with the very first arrival frame (the latest one). + score.Replay.Frames.Clear(); spectatorClient.OnNewFrames += userSentFrames; - seekToGameplay(); } private void userSentFrames(int userId, FrameDataBundle bundle) @@ -62,6 +63,8 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; + bool isFirstBundle = score.Replay.Frames.Count == 0; + foreach (var frame in bundle.Frames) { IConvertibleReplayFrame convertibleFrame = GameplayRuleset.CreateConvertibleReplayFrame(); @@ -73,19 +76,8 @@ namespace osu.Game.Screens.Play score.Replay.Frames.Add(convertedFrame); } - seekToGameplay(); - } - - private bool seekedToGameplay; - - private void seekToGameplay() - { - if (seekedToGameplay || score.Replay.Frames.Count == 0) - return; - - NonFrameStableSeek(score.Replay.Frames[0].Time); - - seekedToGameplay = true; + if (isFirstBundle && score.Replay.Frames.Count > 0) + NonFrameStableSeek(score.Replay.Frames[0].Time); } protected override Score CreateScore() => score; diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index e1cf0cef4e..4a35202df2 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -431,7 +431,7 @@ namespace osu.Game.Screens.Select public class InfoLabel : Container, IHasTooltip { - public string TooltipText { get; } + public LocalisableString TooltipText { get; } public InfoLabel(BeatmapStatistic statistic) { diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 521b90202d..f95ddfee41 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -42,6 +42,7 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate); match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate); match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(Beatmap.BaseDifficulty.CircleSize); + match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(Beatmap.BaseDifficulty.OverallDifficulty); match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(Beatmap.Length); match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(Beatmap.BPM); diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 208048380a..b9e912df8e 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -24,6 +24,7 @@ namespace osu.Game.Screens.Select public OptionalRange ApproachRate; public OptionalRange DrainRate; public OptionalRange CircleSize; + public OptionalRange OverallDifficulty; public OptionalRange Length; public OptionalRange BPM; public OptionalRange BeatDivisor; diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index db2803d29a..72d10019b2 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -51,6 +51,9 @@ namespace osu.Game.Screens.Select case "cs": return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value); + case "od": + return TryUpdateCriteriaRange(ref criteria.OverallDifficulty, op, value); + case "bpm": return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2); diff --git a/osu.Game/Skinning/ResourceStoreBackedSkin.cs b/osu.Game/Skinning/ResourceStoreBackedSkin.cs new file mode 100644 index 0000000000..f041b82cf4 --- /dev/null +++ b/osu.Game/Skinning/ResourceStoreBackedSkin.cs @@ -0,0 +1,57 @@ +// 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 osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Framework.Platform; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + /// + /// An that uses an underlying with namespaces for resources retrieval. + /// + public class ResourceStoreBackedSkin : ISkin, IDisposable + { + private readonly TextureStore textures; + private readonly ISampleStore samples; + + public ResourceStoreBackedSkin(IResourceStore resources, GameHost host, AudioManager audio) + { + textures = new TextureStore(host.CreateTextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); + samples = audio.GetSampleStore(new NamespacedResourceStore(resources, @"Samples")); + } + + public Drawable? GetDrawableComponent(ISkinComponent component) => null; + + public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => textures.Get(componentName, wrapModeS, wrapModeT); + + public ISample? GetSample(ISampleInfo sampleInfo) + { + foreach (var lookup in sampleInfo.LookupNames) + { + ISample? sample = samples.Get(lookup); + if (sample != null) + return sample; + } + + return null; + } + + public IBindable? GetConfig(TLookup lookup) => null; + + public void Dispose() + { + textures.Dispose(); + samples.Dispose(); + } + } +} diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index c48aeca99a..19efc66814 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.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 System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.UI; @@ -42,27 +46,35 @@ namespace osu.Game.Skinning }; } - [Resolved] - private ISkinSource skinSource { get; set; } + private ISkinSource parentSource; - [BackgroundDependencyLoader] - private void load() + private ResourceStoreBackedSkin rulesetResourcesSkin; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - UpdateSkins(); - skinSource.SourceChanged += OnSourceChanged; + parentSource = parent.Get(); + parentSource.SourceChanged += OnSourceChanged; + + if (Ruleset.CreateResourceStore() is IResourceStore resources) + rulesetResourcesSkin = new ResourceStoreBackedSkin(resources, parent.Get(), parent.Get()); + + // ensure sources are populated and ready for use before childrens' asynchronous load flow. + UpdateSkinSources(); + + return base.CreateChildDependencies(parent); } protected override void OnSourceChanged() { - UpdateSkins(); + UpdateSkinSources(); base.OnSourceChanged(); } - protected virtual void UpdateSkins() + protected virtual void UpdateSkinSources() { SkinSources.Clear(); - foreach (var skin in skinSource.AllSources) + foreach (var skin in parentSource.AllSources) { switch (skin) { @@ -75,6 +87,16 @@ namespace osu.Game.Skinning break; } } + + int lastDefaultSkinIndex = SkinSources.IndexOf(SkinSources.OfType().LastOrDefault()); + + // Ruleset resources should be given the ability to override game-wide defaults + // This is achieved by placing them before the last instance of DefaultSkin. + // Note that DefaultSkin may not be present in some test scenes. + if (lastDefaultSkinIndex >= 0) + SkinSources.Insert(lastDefaultSkinIndex, rulesetResourcesSkin); + else + SkinSources.Add(rulesetResourcesSkin); } protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin) @@ -93,8 +115,10 @@ namespace osu.Game.Skinning { base.Dispose(isDisposing); - if (skinSource != null) - skinSource.SourceChanged -= OnSourceChanged; + if (parentSource != null) + parentSource.SourceChanged -= OnSourceChanged; + + rulesetResourcesSkin?.Dispose(); } } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 4cde4cd2b8..ea55fd28c2 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -125,6 +125,8 @@ namespace osu.Game.Skinning private const string unknown_creator_string = "Unknown"; + protected override bool HasCustomHashFunction => true; + protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null) { // we need to populate early to create a hash based off skin.ini contents @@ -142,16 +144,16 @@ namespace osu.Game.Skinning return base.ComputeHash(item, reader); } - protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) + protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) { - await base.Populate(model, archive, cancellationToken).ConfigureAwait(false); - var instance = GetSkin(model); model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo(); if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true) populateMetadata(model, instance); + + return Task.CompletedTask; } private void populateMetadata(SkinInfo item, Skin instance) diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs new file mode 100644 index 0000000000..204c189591 --- /dev/null +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs @@ -0,0 +1,37 @@ +// 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.Database; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Tests.Visual.OnlinePlay; +using osu.Game.Tests.Visual.Spectator; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + /// + /// Interface that defines the dependencies required for multiplayer test scenes. + /// + public interface IMultiplayerTestSceneDependencies : IOnlinePlayTestSceneDependencies + { + /// + /// The cached . + /// + TestMultiplayerClient Client { get; } + + /// + /// The cached . + /// + new TestMultiplayerRoomManager RoomManager { get; } + + /// + /// The cached . + /// + TestUserLookupCache LookupCache { get; } + + /// + /// The cached . + /// + TestSpectatorClient SpectatorClient { get; } + } +} diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index c76d1053b2..b7d3793ab1 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -2,66 +2,55 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.OnlinePlay; +using osu.Game.Tests.Visual.Spectator; namespace osu.Game.Tests.Visual.Multiplayer { - public abstract class MultiplayerTestScene : RoomTestScene + /// + /// The base test scene for all multiplayer components and screens. + /// + public abstract class MultiplayerTestScene : OnlinePlayTestScene, IMultiplayerTestSceneDependencies { public const int PLAYER_1_ID = 55; public const int PLAYER_2_ID = 56; - [Cached(typeof(MultiplayerClient))] - public TestMultiplayerClient Client { get; } + public TestMultiplayerClient Client => OnlinePlayDependencies.Client; + public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager; + public TestUserLookupCache LookupCache => OnlinePlayDependencies?.LookupCache; + public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient; - [Cached(typeof(IRoomManager))] - public TestMultiplayerRoomManager RoomManager { get; } - - [Cached] - public Bindable Filter { get; } - - [Cached] - public OngoingOperationTracker OngoingOperationTracker { get; } - - protected override Container Content => content; - private readonly TestMultiplayerRoomContainer content; + protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; private readonly bool joinRoom; protected MultiplayerTestScene(bool joinRoom = true) { this.joinRoom = joinRoom; - base.Content.Add(content = new TestMultiplayerRoomContainer { RelativeSizeAxes = Axes.Both }); - - Client = content.Client; - RoomManager = content.RoomManager; - Filter = content.Filter; - OngoingOperationTracker = content.OngoingOperationTracker; } [SetUp] public new void Setup() => Schedule(() => { - RoomManager.Schedule(() => RoomManager.PartRoom()); - if (joinRoom) { - Room.Name.Value = "test name"; - Room.Playlist.Add(new PlaylistItem + var room = new Room { - Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value } - }); + Name = { Value = "test name" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + } + } + }; - RoomManager.Schedule(() => RoomManager.CreateRoom(Room)); + RoomManager.CreateRoom(room); + SelectedRoom.Value = room; } }); @@ -72,5 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (joinRoom) AddUntilStep("wait for room join", () => Client.Room != null); } + + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs new file mode 100644 index 0000000000..a2b0b066a7 --- /dev/null +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs @@ -0,0 +1,38 @@ +// 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.Database; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Spectator; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Tests.Visual.OnlinePlay; +using osu.Game.Tests.Visual.Spectator; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + /// + /// Contains the basic dependencies of multiplayer test scenes. + /// + public class MultiplayerTestSceneDependencies : OnlinePlayTestSceneDependencies, IMultiplayerTestSceneDependencies + { + public TestMultiplayerClient Client { get; } + public TestUserLookupCache LookupCache { get; } + public TestSpectatorClient SpectatorClient { get; } + public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager; + + public MultiplayerTestSceneDependencies() + { + Client = new TestMultiplayerClient(RoomManager); + LookupCache = new TestUserLookupCache(); + SpectatorClient = CreateSpectatorClient(); + + CacheAs(Client); + CacheAs(LookupCache); + CacheAs(SpectatorClient); + } + + protected override IRoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); + + protected virtual TestSpectatorClient CreateSpectatorClient() => new TestSpectatorClient(); + } +} diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index b12bd8091d..b0c8d6d19b 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -20,6 +20,9 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { + /// + /// A for use in multiplayer test scenes. Should generally not be used by itself outside of a . + /// public class TestMultiplayerClient : MultiplayerClient { public override IBindable IsConnected => isConnected; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs deleted file mode 100644 index 1abf4d8f5d..0000000000 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs +++ /dev/null @@ -1,48 +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.Game.Online.Multiplayer; -using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.OnlinePlay.Lounge.Components; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestMultiplayerRoomContainer : Container - { - protected override Container Content => content; - private readonly Container content; - - [Cached(typeof(MultiplayerClient))] - public readonly TestMultiplayerClient Client; - - [Cached(typeof(IRoomManager))] - public readonly TestMultiplayerRoomManager RoomManager; - - [Cached] - public readonly Bindable Filter = new Bindable(new FilterCriteria()); - - [Cached] - public readonly OngoingOperationTracker OngoingOperationTracker; - - public TestMultiplayerRoomContainer() - { - RelativeSizeAxes = Axes.Both; - - RoomManager = new TestMultiplayerRoomManager(); - Client = new TestMultiplayerClient(RoomManager); - OngoingOperationTracker = new OngoingOperationTracker(); - - AddRangeInternal(new Drawable[] - { - Client, - RoomManager, - OngoingOperationTracker, - content = new Container { RelativeSizeAxes = Axes.Both } - }); - } - } -} diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index 315be510a3..5d66cdba02 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -11,11 +11,15 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; namespace osu.Game.Tests.Visual.Multiplayer { + /// + /// A for use in multiplayer test scenes. Should generally not be used by itself outside of a . + /// public class TestMultiplayerRoomManager : MultiplayerRoomManager { [Resolved] @@ -29,10 +33,9 @@ namespace osu.Game.Tests.Visual.Multiplayer public new readonly List Rooms = new List(); - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - int currentScoreId = 0; int currentRoomId = 0; int currentPlaylistItemId = 0; diff --git a/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs new file mode 100644 index 0000000000..813e617ac5 --- /dev/null +++ b/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.OnlinePlay +{ + /// + /// A very simple for use in online play test scenes. + /// + public class BasicTestRoomManager : IRoomManager + { + public event Action RoomsUpdated + { + add { } + remove { } + } + + public readonly BindableList Rooms = new BindableList(); + + public IBindable InitialRoomsReceived { get; } = new Bindable(true); + + IBindableList IRoomManager.Rooms => Rooms; + + public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + { + room.RoomID.Value ??= Rooms.Select(r => r.RoomID.Value).Where(id => id != null).Select(id => id.Value).DefaultIfEmpty().Max() + 1; + Rooms.Add(room); + onSuccess?.Invoke(room); + } + + public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => onSuccess?.Invoke(room); + + public void PartRoom() + { + } + + public void AddRooms(int count, RulesetInfo ruleset = null) + { + for (int i = 0; i < count; i++) + { + var room = new Room + { + RoomID = { Value = i }, + Name = { Value = $"Room {i}" }, + Host = { Value = new User { Username = "Host" } }, + EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }, + Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal } + }; + + if (ruleset != null) + { + room.Playlist.Add(new PlaylistItem + { + Ruleset = { Value = ruleset }, + Beatmap = + { + Value = new BeatmapInfo + { + Metadata = new BeatmapMetadata() + } + } + }); + } + + CreateRoom(room); + } + } + } +} diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs new file mode 100644 index 0000000000..6e1e831d9b --- /dev/null +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Lounge.Components; + +namespace osu.Game.Tests.Visual.OnlinePlay +{ + /// + /// Interface that defines the dependencies required for online play test scenes. + /// + public interface IOnlinePlayTestSceneDependencies + { + /// + /// The cached . + /// + Bindable SelectedRoom { get; } + + /// + /// The cached + /// + IRoomManager RoomManager { get; } + + /// + /// The cached . + /// + Bindable Filter { get; } + + /// + /// The cached . + /// + OngoingOperationTracker OngoingOperationTracker { get; } + + /// + /// The cached . + /// + OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } + } +} diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs new file mode 100644 index 0000000000..997c910dd4 --- /dev/null +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -0,0 +1,104 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Lounge.Components; + +namespace osu.Game.Tests.Visual.OnlinePlay +{ + /// + /// A base test scene for all online play components and screens. + /// + public abstract class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies + { + public Bindable SelectedRoom => OnlinePlayDependencies?.SelectedRoom; + public IRoomManager RoomManager => OnlinePlayDependencies?.RoomManager; + public Bindable Filter => OnlinePlayDependencies?.Filter; + public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies?.OngoingOperationTracker; + public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies?.AvailabilityTracker; + + /// + /// All dependencies required for online play components and screens. + /// + protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies?.OnlinePlayDependencies; + + private DelegatedDependencyContainer dependencies; + + protected override Container Content => content; + private readonly Container content; + private readonly Container drawableDependenciesContainer; + + protected OnlinePlayTestScene() + { + base.Content.AddRange(new Drawable[] + { + drawableDependenciesContainer = new Container { RelativeSizeAxes = Axes.Both }, + content = new Container { RelativeSizeAxes = Axes.Both }, + }); + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + dependencies = new DelegatedDependencyContainer(base.CreateChildDependencies(parent)); + return dependencies; + } + + [SetUp] + public void Setup() => Schedule(() => + { + // Reset the room dependencies to a fresh state. + drawableDependenciesContainer.Clear(); + dependencies.OnlinePlayDependencies = CreateOnlinePlayDependencies(); + drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents); + }); + + /// + /// Creates the room dependencies. Called every . + /// + /// + /// Any custom dependencies required for online play sub-classes should be added here. + /// + protected virtual OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new OnlinePlayTestSceneDependencies(); + + /// + /// A providing a mutable lookup source for online play dependencies. + /// + private class DelegatedDependencyContainer : IReadOnlyDependencyContainer + { + /// + /// The online play dependencies. + /// + public OnlinePlayTestSceneDependencies OnlinePlayDependencies { get; set; } + + private readonly IReadOnlyDependencyContainer parent; + private readonly DependencyContainer injectableDependencies; + + /// + /// Creates a new . + /// + /// The fallback to use when cannot satisfy a dependency. + public DelegatedDependencyContainer(IReadOnlyDependencyContainer parent) + { + this.parent = parent; + injectableDependencies = new DependencyContainer(this); + } + + public object Get(Type type) + => OnlinePlayDependencies?.Get(type) ?? parent.Get(type); + + public object Get(Type type, CacheInfo info) + => OnlinePlayDependencies?.Get(type, info) ?? parent.Get(type, info); + + public void Inject(T instance) + where T : class + => injectableDependencies.Inject(instance); + } + } +} diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs new file mode 100644 index 0000000000..ddbbfe501b --- /dev/null +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Lounge.Components; + +namespace osu.Game.Tests.Visual.OnlinePlay +{ + /// + /// Contains the basic dependencies of online play test scenes. + /// + public class OnlinePlayTestSceneDependencies : IReadOnlyDependencyContainer, IOnlinePlayTestSceneDependencies + { + public Bindable SelectedRoom { get; } + public IRoomManager RoomManager { get; } + public Bindable Filter { get; } + public OngoingOperationTracker OngoingOperationTracker { get; } + public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } + + /// + /// All cached dependencies which are also components. + /// + public IReadOnlyList DrawableComponents => drawableComponents; + + private readonly List drawableComponents = new List(); + private readonly DependencyContainer dependencies; + + public OnlinePlayTestSceneDependencies() + { + SelectedRoom = new Bindable(); + RoomManager = CreateRoomManager(); + Filter = new Bindable(new FilterCriteria()); + OngoingOperationTracker = new OngoingOperationTracker(); + AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); + + dependencies = new DependencyContainer(new CachedModelDependencyContainer(null) { Model = { BindTarget = SelectedRoom } }); + + CacheAs(SelectedRoom); + CacheAs(RoomManager); + CacheAs(Filter); + CacheAs(OngoingOperationTracker); + CacheAs(AvailabilityTracker); + } + + public object Get(Type type) + => dependencies.Get(type); + + public object Get(Type type, CacheInfo info) + => dependencies.Get(type, info); + + public void Inject(T instance) + where T : class + => dependencies.Inject(instance); + + protected void Cache(object instance) + { + dependencies.Cache(instance); + if (instance is Drawable drawable) + drawableComponents.Add(drawable); + } + + protected void CacheAs(T instance) + where T : class + { + dependencies.CacheAs(instance); + if (instance is Drawable drawable) + drawableComponents.Add(drawable); + } + + protected virtual IRoomManager CreateRoomManager() => new BasicTestRoomManager(); + } +} diff --git a/osu.Game/Tests/Visual/RoomTestScene.cs b/osu.Game/Tests/Visual/RoomTestScene.cs deleted file mode 100644 index aaf5c7624f..0000000000 --- a/osu.Game/Tests/Visual/RoomTestScene.cs +++ /dev/null @@ -1,33 +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 NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Game.Online.Rooms; - -namespace osu.Game.Tests.Visual -{ - public abstract class RoomTestScene : ScreenTestScene - { - [Cached] - private readonly Bindable currentRoom = new Bindable(); - - protected Room Room => currentRoom.Value; - - private CachedModelDependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - dependencies = new CachedModelDependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Model.BindTo(currentRoom); - return dependencies; - } - - [SetUp] - public void Setup() => Schedule(() => - { - currentRoom.Value = new Room(); - }); - } -} diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index c7aa43b377..f206d4f8b0 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -20,9 +20,15 @@ namespace osu.Game.Tests.Visual.Spectator { public class TestSpectatorClient : SpectatorClient { + /// + /// Maximum number of frames sent per bundle via . + /// + public const int FRAME_BUNDLE_SIZE = 10; + public override IBindable IsConnected { get; } = new Bindable(true); private readonly Dictionary userBeatmapDictionary = new Dictionary(); + private readonly Dictionary userNextFrameDictionary = new Dictionary(); [Resolved] private IAPIProvider api { get; set; } = null!; @@ -35,6 +41,7 @@ namespace osu.Game.Tests.Visual.Spectator public void StartPlay(int userId, int beatmapId) { userBeatmapDictionary[userId] = beatmapId; + userNextFrameDictionary[userId] = 0; sendPlayingState(userId); } @@ -57,24 +64,41 @@ namespace osu.Game.Tests.Visual.Spectator public new void Schedule(Action action) => base.Schedule(action); /// - /// Sends frames for an arbitrary user. + /// Sends frames for an arbitrary user, in bundles containing 10 frames each. /// /// The user to send frames for. - /// The frame index. - /// The number of frames to send. - public void SendFrames(int userId, int index, int count) + /// The total number of frames to send. + public void SendFrames(int userId, int count) { var frames = new List(); - for (int i = index; i < index + count; i++) - { - var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1; + int currentFrameIndex = userNextFrameDictionary[userId]; + int lastFrameIndex = currentFrameIndex + count - 1; - frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); + for (; currentFrameIndex <= lastFrameIndex; currentFrameIndex++) + { + // This is done in the next frame so that currentFrameIndex is updated to the correct value. + if (frames.Count == FRAME_BUNDLE_SIZE) + flush(); + + var buttonState = currentFrameIndex == lastFrameIndex ? ReplayButtonState.None : ReplayButtonState.Left1; + frames.Add(new LegacyReplayFrame(currentFrameIndex * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); } - var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames); - ((ISpectatorClient)this).UserSentFrames(userId, bundle); + flush(); + + userNextFrameDictionary[userId] = currentFrameIndex; + + void flush() + { + if (frames.Count == 0) + return; + + var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, frames.ToArray()); + ((ISpectatorClient)this).UserSentFrames(userId, bundle); + + frames.Clear(); + } } protected override Task BeginPlayingInternal(SpectatorState state) diff --git a/osu.Game/Tests/Visual/TestUserLookupCache.cs b/osu.Game/Tests/Visual/TestUserLookupCache.cs new file mode 100644 index 0000000000..d2941b5bd5 --- /dev/null +++ b/osu.Game/Tests/Visual/TestUserLookupCache.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Database; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual +{ + public class TestUserLookupCache : UserLookupCache + { + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User + { + Id = lookup, + Username = $"User {lookup}" + }); + } +} diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index c3bf740108..f73489ac61 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Containers; namespace osu.Game.Users.Drawables @@ -68,11 +69,11 @@ namespace osu.Game.Users.Drawables private class ClickableArea : OsuClickableContainer { - private string tooltip = default_tooltip_text; + private LocalisableString tooltip = default_tooltip_text; - public override string TooltipText + public override LocalisableString TooltipText { - get => Enabled.Value ? tooltip : null; + get => Enabled.Value ? tooltip : default; set => tooltip = value; } diff --git a/osu.Game/Users/Drawables/DrawableFlag.cs b/osu.Game/Users/Drawables/DrawableFlag.cs index 1d648e46b6..aea40a01ae 100644 --- a/osu.Game/Users/Drawables/DrawableFlag.cs +++ b/osu.Game/Users/Drawables/DrawableFlag.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; namespace osu.Game.Users.Drawables { @@ -13,7 +14,7 @@ namespace osu.Game.Users.Drawables { private readonly Country country; - public string TooltipText => country?.FullName; + public LocalisableString TooltipText => country?.FullName; public DrawableFlag(Country country) { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f91620bd25..589afb86be 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 22c4340ba2..a8bf0e4ab2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - +