diff --git a/osu.Android.props b/osu.Android.props index ec223f98c2..8fd761691c 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,8 +51,8 @@ - - + + diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs index efc3f21149..dbfd170ea1 100644 --- a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs +++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs @@ -5,23 +5,23 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; -using osu.Game; using osu.Game.Configuration; +using osu.Game.Screens.Play; namespace osu.Desktop.Windows { public class GameplayWinKeyBlocker : Component { private Bindable disableWinKey; - private Bindable localUserPlaying; + private IBindable localUserPlaying; [Resolved] private GameHost host { get; set; } [BackgroundDependencyLoader] - private void load(OsuGame game, OsuConfigManager config) + private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config) { - localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); + localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy(); localUserPlaying.BindValueChanged(_ => updateBlocking()); disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); diff --git a/osu.Game.Benchmarks/BenchmarkMod.cs b/osu.Game.Benchmarks/BenchmarkMod.cs new file mode 100644 index 0000000000..050ddf36d5 --- /dev/null +++ b/osu.Game.Benchmarks/BenchmarkMod.cs @@ -0,0 +1,34 @@ +// 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 BenchmarkDotNet.Attributes; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Benchmarks +{ + public class BenchmarkMod : BenchmarkTest + { + private OsuModDoubleTime mod; + + [Params(1, 10, 100)] + public int Times { get; set; } + + [GlobalSetup] + public void GlobalSetup() + { + mod = new OsuModDoubleTime(); + } + + [Benchmark] + public int ModHashCode() + { + var hashCode = new HashCode(); + + for (int i = 0; i < Times; i++) + hashCode.Add(mod); + + return hashCode.ToHashCode(); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 4372ed938c..cfb3fe40be 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Difficulty.Skills { - public class Movement : StrainSkill + public class Movement : StrainDecaySkill { private const float absolute_player_positioning_error = 16f; private const float normalized_hitobject_radius = 41.0f; diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 2ba2ee6b4a..01d930d585 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Difficulty.Skills { - public class Strain : StrainSkill + public class Strain : StrainDecaySkill { private const double individual_decay_base = 0.125; private const double overall_decay_base = 0.30; @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills return individualStrain + overallStrain - CurrentStrain; } - protected override double GetPeakStrain(double offset) + protected override double CalculateInitialStrain(double offset) => applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base) + applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index e47edc37cc..7bcd867a9c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -10,7 +10,7 @@ using osu.Framework.Utils; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { - public abstract class OsuStrainSkill : StrainSkill + public abstract class OsuStrainSkill : StrainDecaySkill { /// /// The number of sections with the highest strains, which the peak strain reductions will apply to. diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index d4f1602a46..c89527d8bd 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -64,11 +64,14 @@ namespace osu.Game.Rulesets.Osu.Edit if (hitObject is DrawableHitCircle circle) { - circle.ApproachCircle - .FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4) - .Expire(); + using (circle.BeginAbsoluteSequence(circle.HitStateUpdateTime)) + { + circle.ApproachCircle + .FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4) + .Expire(); - circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint); + circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint); + } } if (hitObject is IHasMainCirclePiece mainPieceContainer) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs index 587ff4b573..75d847d54d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs @@ -62,6 +62,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override bool InterpolateMovements => !disjointTrail; protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1); + protected override bool AvoidDrawingNearCursor => !disjointTrail; protected override void Update() { diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 7a95111c91..62cab4d6d7 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -138,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor protected virtual bool InterpolateMovements => true; protected virtual float IntervalMultiplier => 1.0f; + protected virtual bool AvoidDrawingNearCursor => false; private Vector2? lastPosition; private readonly InputResampler resampler = new InputResampler(); @@ -171,8 +172,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Vector2 direction = diff / distance; float interval = partSize.X / 2.5f * IntervalMultiplier; + float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0); - for (float d = interval; d < distance; d += interval) + for (float d = interval; d < stopAt; d += interval) { lastPosition = pos1 + direction * d; addPart(lastPosition.Value); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 769d021362..0c17ca66b9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Calculates the colour coefficient of taiko difficulty. /// - public class Colour : StrainSkill + public class Colour : StrainDecaySkill { protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index a32f6ebe0d..973e55f4b4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Calculates the rhythm coefficient of taiko difficulty. /// - public class Rhythm : StrainSkill + public class Rhythm : StrainDecaySkill { protected override double SkillMultiplier => 10; protected override double StrainDecayBase => 0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 4cceadb23f..54cf233d69 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit). /// - public class Stamina : StrainSkill + public class Stamina : StrainDecaySkill { protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyCacheTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyCacheTest.cs deleted file mode 100644 index d407c0663f..0000000000 --- a/osu.Game.Tests/Beatmaps/BeatmapDifficultyCacheTest.cs +++ /dev/null @@ -1,56 +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.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Mods; - -namespace osu.Game.Tests.Beatmaps -{ - [TestFixture] - public class BeatmapDifficultyCacheTest - { - [Test] - public void TestKeyEqualsWithDifferentModInstances() - { - var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); - var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); - - Assert.That(key1, Is.EqualTo(key2)); - } - - [Test] - public void TestKeyEqualsWithDifferentModOrder() - { - var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); - var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); - - Assert.That(key1, Is.EqualTo(key2)); - } - - [TestCase(1.3, DifficultyRating.Easy)] - [TestCase(1.993, DifficultyRating.Easy)] - [TestCase(1.998, DifficultyRating.Normal)] - [TestCase(2.4, DifficultyRating.Normal)] - [TestCase(2.693, DifficultyRating.Normal)] - [TestCase(2.698, DifficultyRating.Hard)] - [TestCase(3.5, DifficultyRating.Hard)] - [TestCase(3.993, DifficultyRating.Hard)] - [TestCase(3.997, DifficultyRating.Insane)] - [TestCase(5.0, DifficultyRating.Insane)] - [TestCase(5.292, DifficultyRating.Insane)] - [TestCase(5.297, DifficultyRating.Expert)] - [TestCase(6.2, DifficultyRating.Expert)] - [TestCase(6.493, DifficultyRating.Expert)] - [TestCase(6.498, DifficultyRating.ExpertPlus)] - [TestCase(8.3, DifficultyRating.ExpertPlus)] - public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket) - { - var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating); - - Assert.AreEqual(expectedBracket, actualBracket); - } - } -} diff --git a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs new file mode 100644 index 0000000000..da457c9e8f --- /dev/null +++ b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs @@ -0,0 +1,146 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Beatmaps +{ + [HeadlessTest] + public class TestSceneBeatmapDifficultyCache : OsuTestScene + { + public const double BASE_STARS = 5.55; + + private BeatmapSetInfo importedSet; + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + private TestBeatmapDifficultyCache difficultyCache; + + private IBindable starDifficultyBindable; + + [BackgroundDependencyLoader] + private void load(OsuGameBase osu) + { + importedSet = ImportBeatmapTest.LoadQuickOszIntoOsu(osu).Result; + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("setup difficulty cache", () => + { + SelectedMods.Value = Array.Empty(); + + Child = difficultyCache = new TestBeatmapDifficultyCache(); + + starDifficultyBindable = difficultyCache.GetBindableDifficulty(importedSet.Beatmaps.First()); + }); + + AddUntilStep($"star difficulty -> {BASE_STARS}", () => starDifficultyBindable.Value?.Stars == BASE_STARS); + } + + [Test] + public void TestStarDifficultyChangesOnModSettings() + { + OsuModDoubleTime dt = null; + + AddStep("change selected mod to DT", () => SelectedMods.Value = new[] { dt = new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } }); + AddUntilStep($"star difficulty -> {BASE_STARS + 1.5}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.5); + + AddStep("change DT speed to 1.25", () => dt.SpeedChange.Value = 1.25); + AddUntilStep($"star difficulty -> {BASE_STARS + 1.25}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.25); + + AddStep("change selected mod to NC", () => SelectedMods.Value = new[] { new OsuModNightcore { SpeedChange = { Value = 1.75 } } }); + AddUntilStep($"star difficulty -> {BASE_STARS + 1.75}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.75); + } + + [Test] + public void TestKeyEqualsWithDifferentModInstances() + { + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + + Assert.That(key1, Is.EqualTo(key2)); + Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode())); + } + + [Test] + public void TestKeyEqualsWithDifferentModOrder() + { + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); + + Assert.That(key1, Is.EqualTo(key2)); + Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode())); + } + + [Test] + public void TestKeyDoesntEqualWithDifferentModSettings() + { + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } }); + + Assert.That(key1, Is.Not.EqualTo(key2)); + Assert.That(key1.GetHashCode(), Is.Not.EqualTo(key2.GetHashCode())); + } + + [Test] + public void TestKeyEqualWithMatchingModSettings() + { + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }); + + Assert.That(key1, Is.EqualTo(key2)); + Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode())); + } + + [TestCase(1.3, DifficultyRating.Easy)] + [TestCase(1.993, DifficultyRating.Easy)] + [TestCase(1.998, DifficultyRating.Normal)] + [TestCase(2.4, DifficultyRating.Normal)] + [TestCase(2.693, DifficultyRating.Normal)] + [TestCase(2.698, DifficultyRating.Hard)] + [TestCase(3.5, DifficultyRating.Hard)] + [TestCase(3.993, DifficultyRating.Hard)] + [TestCase(3.997, DifficultyRating.Insane)] + [TestCase(5.0, DifficultyRating.Insane)] + [TestCase(5.292, DifficultyRating.Insane)] + [TestCase(5.297, DifficultyRating.Expert)] + [TestCase(6.2, DifficultyRating.Expert)] + [TestCase(6.493, DifficultyRating.Expert)] + [TestCase(6.498, DifficultyRating.ExpertPlus)] + [TestCase(8.3, DifficultyRating.ExpertPlus)] + public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket) + { + var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating); + + Assert.AreEqual(expectedBracket, actualBracket); + } + + private class TestBeatmapDifficultyCache : BeatmapDifficultyCache + { + protected override Task ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default) + { + var rateAdjust = lookup.OrderedMods.OfType().SingleOrDefault(); + if (rateAdjust != null) + return Task.FromResult(new StarDifficulty(BASE_STARS + rateAdjust.SpeedChange.Value, 0)); + + return Task.FromResult(new StarDifficulty(BASE_STARS, 0)); + } + } + } +} diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index 8f5ebf53bd..d87ac29d75 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Collections.IO @@ -127,7 +128,7 @@ namespace osu.Game.Tests.Collections.IO [Test] public async Task TestSaveAndReload() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new TestRunHeadlessGameHost("TestSaveAndReload", bypassCleanup: true)) { try { @@ -148,7 +149,7 @@ namespace osu.Game.Tests.Collections.IO } } - using (HeadlessGameHost host = new HeadlessGameHost("TestSaveAndReload")) + using (HeadlessGameHost host = new TestRunHeadlessGameHost("TestSaveAndReload")) { try { diff --git a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs index 27cece42e8..b612899d79 100644 --- a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs +++ b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs @@ -8,7 +8,7 @@ using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Input; -using osu.Game.Tests.Visual.Navigation; +using osu.Game.Tests.Visual; namespace osu.Game.Tests.Input { diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 4c44e2ec72..5e14af5c27 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -6,10 +6,10 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; using NUnit.Framework; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.IO; @@ -278,7 +278,7 @@ namespace osu.Game.Tests.NonVisual private static string getDefaultLocationFor(string testTypeName) { - string path = Path.Combine(RuntimeInfo.StartupDirectory, "headless", testTypeName); + string path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, testTypeName); if (Directory.Exists(path)) Directory.Delete(path, true); @@ -288,7 +288,7 @@ namespace osu.Game.Tests.NonVisual private string prepareCustomPath(string suffix = "") { - string path = Path.Combine(RuntimeInfo.StartupDirectory, $"custom-path{suffix}"); + string path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path{suffix}"); if (Directory.Exists(path)) Directory.Delete(path, true); @@ -308,6 +308,19 @@ namespace osu.Game.Tests.NonVisual InitialStorage = new DesktopStorage(defaultStorageLocation, this); InitialStorage.DeleteDirectory(string.Empty); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + try + { + // the storage may have changed from the initial location. + // this handles cleanup of the initial location. + InitialStorage.DeleteDirectory(string.Empty); + } + catch { } + } } } } diff --git a/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs b/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs new file mode 100644 index 0000000000..5491774e26 --- /dev/null +++ b/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs @@ -0,0 +1,72 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using MessagePack; +using NUnit.Framework; +using osu.Game.Online; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; + +namespace osu.Game.Tests.Online +{ + [TestFixture] + public class TestMultiplayerMessagePackSerialization + { + [Test] + public void TestSerialiseRoom() + { + var room = new MultiplayerRoom(1) + { + MatchState = new TeamVersusRoomState() + }; + + var serialized = MessagePackSerializer.Serialize(room); + + var deserialized = MessagePackSerializer.Deserialize(serialized); + + Assert.IsTrue(deserialized.MatchState is TeamVersusRoomState); + } + + [Test] + public void TestSerialiseUserStateExpected() + { + var state = new TeamVersusUserState(); + + var serialized = MessagePackSerializer.Serialize(typeof(MatchUserState), state); + var deserialized = MessagePackSerializer.Deserialize(serialized); + + Assert.IsTrue(deserialized is TeamVersusUserState); + } + + [Test] + public void TestSerialiseUnionFailsWithSingalR() + { + var state = new TeamVersusUserState(); + + // SignalR serialises using the actual type, rather than a base specification. + var serialized = MessagePackSerializer.Serialize(typeof(TeamVersusUserState), state); + + // works with explicit type specified. + MessagePackSerializer.Deserialize(serialized); + + // fails with base (union) type. + Assert.Throws(() => MessagePackSerializer.Deserialize(serialized)); + } + + [Test] + public void TestSerialiseUnionSucceedsWithWorkaround() + { + var state = new TeamVersusUserState(); + + // SignalR serialises using the actual type, rather than a base specification. + var serialized = MessagePackSerializer.Serialize(typeof(TeamVersusUserState), state, SignalRUnionWorkaroundResolver.OPTIONS); + + // works with explicit type specified. + MessagePackSerializer.Deserialize(serialized); + + // works with custom resolver. + var deserialized = MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS); + Assert.IsTrue(deserialized is TeamVersusUserState); + } + } +} diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 8124bd4199..bab8dfc983 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -50,10 +50,10 @@ namespace osu.Game.Tests.Skins.IO var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin2.osk")); Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); - Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(1)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).Count, Is.EqualTo(1)); // the first should be overwritten by the second import. - Assert.That(osu.Dependencies.Get().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); } finally { @@ -76,10 +76,10 @@ namespace osu.Game.Tests.Skins.IO var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk")); Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); - Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(2)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).Count, Is.EqualTo(2)); - Assert.That(osu.Dependencies.Get().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID)); - Assert.That(osu.Dependencies.Get().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); } finally { @@ -101,10 +101,10 @@ namespace osu.Game.Tests.Skins.IO var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2.1", "skinner"), "skin2.osk")); Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); - Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(2)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).Count, Is.EqualTo(2)); - Assert.That(osu.Dependencies.Get().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID)); - Assert.That(osu.Dependencies.Get().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins(true).Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); } finally { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index 0b1617b6a6..0abf0c47f8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -78,6 +78,24 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); } + [Test] + public void TestClampWhenSeekOutsideBeatmapBounds() + { + AddStep("stop clock", Clock.Stop); + + AddStep("seek before start time", () => Clock.Seek(-1000)); + AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0); + + AddStep("seek beyond track length", () => Clock.Seek(Clock.TrackLength + 1000)); + AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength); + + AddStep("seek smoothly before start time", () => Clock.SeekSmoothlyTo(-1000)); + AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0); + + AddStep("seek smoothly beyond track length", () => Clock.SeekSmoothlyTo(Clock.TrackLength + 1000)); + AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength); + } + protected override void Dispose(bool isDisposing) { Beatmap.Disabled = false; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 3017428039..290ba3317b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -19,6 +19,8 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneHUDOverlay : OsuManualInputManagerTestScene { + private OsuConfigManager localConfig; + private HUDOverlay hudOverlay; [Cached] @@ -31,8 +33,14 @@ namespace osu.Game.Tests.Visual.Gameplay private Drawable hideTarget => hudOverlay.KeyCounter; private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First(); - [Resolved] - private OsuConfigManager config { get; set; } + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage)); + } + + [SetUp] + public void SetUp() => Schedule(() => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always)); [Test] public void TestComboCounterIncrementing() @@ -85,11 +93,7 @@ namespace osu.Game.Tests.Visual.Gameplay { createNew(); - HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringGameplay; - - AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode)); - - AddStep("set hud to never show", () => config.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never)); + AddStep("set hud to never show", () => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never)); AddUntilStep("wait for fade", () => !hideTarget.IsPresent); @@ -98,37 +102,28 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("stop trigering", () => InputManager.ReleaseKey(Key.ControlLeft)); AddUntilStep("wait for fade", () => !hideTarget.IsPresent); - - AddStep("set original config value", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue)); } [Test] public void TestExternalHideDoesntAffectConfig() { - HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringGameplay; - createNew(); - AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode)); - AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); - AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.HUDVisibilityMode)); + AddAssert("config unchanged", () => localConfig.GetBindable(OsuSetting.HUDVisibilityMode).IsDefault); AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); - AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.HUDVisibilityMode)); + AddAssert("config unchanged", () => localConfig.GetBindable(OsuSetting.HUDVisibilityMode).IsDefault); } [Test] public void TestChangeHUDVisibilityOnHiddenKeyCounter() { - bool keyCounterVisibleValue = false; - createNew(); - AddStep("save keycounter visible value", () => keyCounterVisibleValue = config.Get(OsuSetting.KeyOverlay)); - AddStep("set keycounter visible false", () => + AddStep("hide key overlay", () => { - config.SetValue(OsuSetting.KeyOverlay, false); + localConfig.SetValue(OsuSetting.KeyOverlay, false); hudOverlay.KeyCounter.AlwaysVisible.Value = false; }); @@ -139,24 +134,16 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent); AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); - - AddStep("return value", () => config.SetValue(OsuSetting.KeyOverlay, keyCounterVisibleValue)); } [Test] public void TestHiddenHUDDoesntBlockSkinnableComponentsLoad() { - HUDVisibilityMode originalConfigValue = default; - - AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode)); - - AddStep("set hud to never show", () => config.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never)); + AddStep("set hud to never show", () => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never)); createNew(); AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType().Single().ComponentsLoaded); - - AddStep("set original config value", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue)); } private void createNew(Action action = null) @@ -175,5 +162,11 @@ namespace osu.Game.Tests.Visual.Gameplay Child = hudOverlay; }); } + + protected override void Dispose(bool isDisposing) + { + localConfig?.Dispose(); + base.Dispose(isDisposing); + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 4fa4c00981..b308f3d7d8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -3,7 +3,6 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Bindables; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -66,7 +65,6 @@ namespace osu.Game.Tests.Visual.Gameplay protected class OverlayTestPlayer : TestPlayer { public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value; - public new Bindable LocalUserPlaying => base.LocalUserPlaying; } } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index aaf3323432..9037338e23 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -10,7 +10,6 @@ using osu.Game.Database; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Tests.Resources; -using osu.Game.Tests.Visual.Navigation; namespace osu.Game.Tests.Visual.Menus { diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs index e58f85b0b3..e34ec6c46a 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs @@ -1,10 +1,13 @@ // 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 NUnit.Framework; using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Overlays; -using osu.Game.Tests.Visual.Navigation; namespace osu.Game.Tests.Visual.Menus { @@ -22,21 +25,48 @@ namespace osu.Game.Tests.Visual.Menus [Test] public void TestScreenOffsettingOnSettingsOverlay() { - AddStep("open settings", () => Game.Settings.Show()); - AddUntilStep("right screen offset applied", () => Game.ScreenOffsetContainer.X == SettingsPanel.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO); + foreach (var scalingMode in Enum.GetValues(typeof(ScalingMode)).Cast()) + { + AddStep($"set scaling mode to {scalingMode}", () => + { + Game.LocalConfig.SetValue(OsuSetting.Scaling, scalingMode); - AddStep("hide settings", () => Game.Settings.Hide()); - AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f); + if (scalingMode != ScalingMode.Off) + { + Game.LocalConfig.SetValue(OsuSetting.ScalingSizeX, 0.5f); + Game.LocalConfig.SetValue(OsuSetting.ScalingSizeY, 0.5f); + } + }); + + AddStep("open settings", () => Game.Settings.Show()); + AddUntilStep("right screen offset applied", () => Precision.AlmostEquals(Game.ScreenOffsetContainer.X, SettingsPanel.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO)); + + AddStep("hide settings", () => Game.Settings.Hide()); + AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f); + } } [Test] public void TestScreenOffsettingOnNotificationOverlay() { - AddStep("open notifications", () => Game.Notifications.Show()); - AddUntilStep("right screen offset applied", () => Game.ScreenOffsetContainer.X == -NotificationOverlay.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO); + foreach (var scalingMode in Enum.GetValues(typeof(ScalingMode)).Cast()) + { + if (scalingMode != ScalingMode.Off) + { + AddStep($"set scaling mode to {scalingMode}", () => + { + Game.LocalConfig.SetValue(OsuSetting.Scaling, scalingMode); + Game.LocalConfig.SetValue(OsuSetting.ScalingSizeX, 0.5f); + Game.LocalConfig.SetValue(OsuSetting.ScalingSizeY, 0.5f); + }); + } - AddStep("hide notifications", () => Game.Notifications.Hide()); - AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f); + AddStep("open notifications", () => Game.Notifications.Show()); + AddUntilStep("right screen offset applied", () => Precision.AlmostEquals(Game.ScreenOffsetContainer.X, -NotificationOverlay.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO)); + + AddStep("hide notifications", () => Game.Notifications.Hide()); + AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f); + } } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index 299bbacf08..8ca578b592 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -10,11 +10,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Overlays; using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Tests.Beatmaps; using osu.Game.Users; @@ -108,12 +108,6 @@ namespace osu.Game.Tests.Visual.Multiplayer EndDate = { Value = DateTimeOffset.Now }, }), createDrawableRoom(new Room - { - Name = { Value = "Room 4 (realtime)" }, - Status = { Value = new RoomStatusOpen() }, - Category = { Value = RoomCategory.Realtime }, - }), - createDrawableRoom(new Room { Name = { Value = "Room 4 (spotlight)" }, Status = { Value = new RoomStatusOpen() }, @@ -134,7 +128,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Name = { Value = "Room with password" }, Status = { Value = new RoomStatusOpen() }, - Category = { Value = RoomCategory.Realtime }, + Type = { Value = MatchType.HeadToHead }, })); AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); @@ -159,10 +153,7 @@ namespace osu.Game.Tests.Visual.Multiplayer })); } - var drawableRoom = new DrawableRoom(room) { MatchingFilter = true }; - drawableRoom.Action = () => drawableRoom.State = drawableRoom.State == SelectionState.Selected ? SelectionState.NotSelected : SelectionState.Selected; - - return drawableRoom; + return new DrawableLoungeRoom(room) { MatchingFilter = true }; } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs new file mode 100644 index 0000000000..eadfc9b279 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs @@ -0,0 +1,131 @@ +// 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 Moq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Play; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneGameplayChatDisplay : MultiplayerTestScene + { + private GameplayChatDisplay chatDisplay; + + [Cached(typeof(ILocalUserPlayInfo))] + private ILocalUserPlayInfo localUserInfo; + + private readonly Bindable localUserPlaying = new Bindable(); + + private TextBox textBox => chatDisplay.ChildrenOfType().First(); + + public TestSceneGameplayChatDisplay() + { + var mockLocalUserInfo = new Mock(); + mockLocalUserInfo.SetupGet(i => i.IsPlaying).Returns(localUserPlaying); + + localUserInfo = mockLocalUserInfo.Object; + } + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("load chat display", () => Child = chatDisplay = new GameplayChatDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + }); + + AddStep("expand", () => chatDisplay.Expanded.Value = true); + } + + [Test] + public void TestCantClickWhenPlaying() + { + setLocalUserPlaying(true); + + AddStep("attempt focus chat", () => + { + InputManager.MoveMouseTo(textBox); + InputManager.Click(MouseButton.Left); + }); + + assertChatFocused(false); + } + + [Test] + public void TestFocusDroppedWhenPlaying() + { + assertChatFocused(false); + + AddStep("focus chat", () => + { + InputManager.MoveMouseTo(textBox); + InputManager.Click(MouseButton.Left); + }); + + setLocalUserPlaying(true); + assertChatFocused(false); + + // should still stay non-focused even after entering a new break section. + setLocalUserPlaying(false); + assertChatFocused(false); + } + + [Test] + public void TestFocusOnTabKeyWhenExpanded() + { + setLocalUserPlaying(true); + + assertChatFocused(false); + AddStep("press tab", () => InputManager.Key(Key.Tab)); + assertChatFocused(true); + } + + [Test] + public void TestFocusOnTabKeyWhenNotExpanded() + { + AddStep("set not expanded", () => chatDisplay.Expanded.Value = false); + AddUntilStep("is not visible", () => !chatDisplay.IsPresent); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + assertChatFocused(true); + AddUntilStep("is visible", () => chatDisplay.IsPresent); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + assertChatFocused(false); + AddUntilStep("is not visible", () => !chatDisplay.IsPresent); + } + + [Test] + public void TestFocusToggleViaAction() + { + AddStep("set not expanded", () => chatDisplay.Expanded.Value = false); + AddUntilStep("is not visible", () => !chatDisplay.IsPresent); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + assertChatFocused(true); + AddUntilStep("is visible", () => chatDisplay.IsPresent); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + assertChatFocused(false); + AddUntilStep("is not visible", () => !chatDisplay.IsPresent); + } + + private void assertChatFocused(bool isFocused) => + AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused); + + private void setLocalUserPlaying(bool playing) => + AddStep($"local user {(playing ? "playing" : "not playing")}", () => localUserPlaying.Value = playing); + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index bcbdcd2a4f..2f895d2157 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -9,6 +9,7 @@ using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Input; @@ -17,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { - protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager; + protected new TestRequestHandlingRoomManager RoomManager => (TestRequestHandlingRoomManager)base.RoomManager; private RoomsContainer container; @@ -38,7 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add rooms", () => RoomManager.AddRooms(3)); AddAssert("has 3 rooms", () => container.Rooms.Count == 3); - AddStep("remove first room", () => RoomManager.Rooms.Remove(RoomManager.Rooms.FirstOrDefault())); + AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.FirstOrDefault())); AddAssert("has 2 rooms", () => container.Rooms.Count == 2); AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0)); @@ -74,7 +75,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var room = RoomManager.Rooms[1]; RoomManager.RemoveRoom(room); - RoomManager.AddRoom(room); + RoomManager.AddOrUpdateRoom(room); }); AddAssert("no selection", () => checkRoomSelected(null)); @@ -115,11 +116,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); - AddStep("filter one room", () => container.Filter(new FilterCriteria { SearchString = "1" })); + AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" }); AddUntilStep("1 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 1); - AddStep("remove filter", () => container.Filter(null)); + AddStep("remove filter", () => container.Filter.Value = null); AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); } @@ -131,13 +132,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo)); // Todo: What even is this case...? - AddStep("set empty filter criteria", () => container.Filter(null)); + AddStep("set empty filter criteria", () => container.Filter.Value = null); AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5); - AddStep("filter osu! rooms", () => container.Filter(new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo })); + AddStep("filter osu! rooms", () => container.Filter.Value = 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 })); + AddStep("filter catch rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo }); AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3); } @@ -150,6 +151,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private bool checkRoomSelected(Room room) => SelectedRoom.Value == room; private Room getRoomInFlow(int index) => - (container.ChildrenOfType>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room; + (container.ChildrenOfType>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room; } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs deleted file mode 100644 index 71ba5db481..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ /dev/null @@ -1,55 +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.Game.Beatmaps; -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 : OnlinePlayTestScene - { - [SetUp] - public new void Setup() => Schedule(() => - { - SelectedRoom.Value = new Room - { - Name = { Value = "A very awesome room" }, - Host = { Value = new User { Id = 2, Username = "peppy" } }, - Playlist = - { - new PlaylistItem - { - Beatmap = - { - Value = new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Title = "Title", - Artist = "Artist", - AuthorString = "Author", - }, - Version = "Version", - Ruleset = new OsuRuleset().RulesetInfo - } - }, - RequiredMods = - { - new OsuModDoubleTime(), - new OsuModNoFail(), - new OsuModRelax(), - } - } - } - }; - - Child = new Header(); - }); - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 65b1d6d53a..18e4a6c575 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -6,13 +6,17 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Rulesets.UI; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Users; @@ -23,6 +27,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private OsuGameBase game { get; set; } + [Resolved] + private OsuConfigManager config { get; set; } + [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -80,6 +87,32 @@ namespace osu.Game.Tests.Visual.Multiplayer AddWaitStep("wait a bit", 20); } + [Test] + public void TestSpectatorPlayerInteractiveElementsHidden() + { + HUDVisibilityMode originalConfigValue = default; + + AddStep("get original config hud visibility", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode)); + AddStep("set config hud visibility to always", () => config.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always)); + + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); + loadSpectateScreen(false); + + AddUntilStep("wait for player loaders", () => this.ChildrenOfType().Count() == 2); + AddAssert("all player loader settings hidden", () => this.ChildrenOfType().All(l => !l.ChildrenOfType>().Any())); + + AddUntilStep("wait for players to load", () => spectatorScreen.AllPlayersLoaded); + + // components wrapped in skinnable target containers load asynchronously, potentially taking more than one frame to load. + // therefore use until step rather than direct assert to account for that. + AddUntilStep("all interactive elements removed", () => this.ChildrenOfType().All(p => + !p.ChildrenOfType().Any() && + !p.ChildrenOfType().Any() && + p.ChildrenOfType().SingleOrDefault()?.ShowHandle == false)); + + AddStep("restore config hud visibility", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue)); + } + [Test] public void TestTeamDisplay() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 08b3fb98a8..6c1aed71e6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -44,6 +44,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayer multiplayerScreen; private TestMultiplayerClient client; + private TestRequestHandlingMultiplayerRoomManager roomManager => multiplayerScreen.RoomManager; + [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestUserLookupCache(); @@ -68,7 +70,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("load dependencies", () => { - client = new TestMultiplayerClient(multiplayerScreen.RoomManager); + client = new TestMultiplayerClient(roomManager); // The screen gets suspended so it stops receiving updates. Child = client; @@ -132,39 +134,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestExitMidJoin() { - Room room = null; - AddStep("create room", () => { - room = new Room - { - Name = { Value = "Test Room" }, - Playlist = - { - new PlaylistItem - { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - } - } - }; - }); - - AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria()); - AddStep("select room", () => InputManager.Key(Key.Down)); - AddStep("join room and immediately exit", () => - { - multiplayerScreen.ChildrenOfType().Single().Open(room); - Schedule(() => Stack.CurrentScreen.Exit()); - }); - } - - [Test] - public void TestJoinRoomWithoutPassword() - { - AddStep("create room", () => - { - multiplayerScreen.RoomManager.AddRoom(new Room + roomManager.AddServerSideRoom(new Room { Name = { Value = "Test Room" }, Playlist = @@ -178,7 +150,39 @@ namespace osu.Game.Tests.Visual.Multiplayer }); }); - AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria()); + AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter()); + AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); + + AddStep("select room", () => InputManager.Key(Key.Down)); + AddStep("join room and immediately exit select", () => + { + InputManager.Key(Key.Enter); + Schedule(() => Stack.CurrentScreen.Exit()); + }); + } + + [Test] + public void TestJoinRoomWithoutPassword() + { + AddStep("create room", () => + { + roomManager.AddServerSideRoom(new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + }); + + AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter()); + AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); + AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); @@ -211,7 +215,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - multiplayerScreen.RoomManager.AddRoom(new Room + roomManager.AddServerSideRoom(new Room { Name = { Value = "Test Room" }, Password = { Value = "password" }, @@ -226,12 +230,14 @@ namespace osu.Game.Tests.Visual.Multiplayer }); }); - AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria()); + AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter()); + AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); + AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); - DrawableRoom.PasswordEntryPopover passwordEntryPopover = null; - AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); + DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null; + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index c66d5429d6..99f6ab1ae1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -9,7 +9,6 @@ using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge; -using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Input; @@ -18,7 +17,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene { - protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager; + protected new TestRequestHandlingRoomManager RoomManager => (TestRequestHandlingRoomManager)base.RoomManager; private LoungeSubScreen loungeScreen; @@ -59,20 +58,20 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); - AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any()); + AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any()); AddStep("exit screen", () => Stack.Exit()); - AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); + AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); } [Test] public void TestJoinRoomWithPassword() { - DrawableRoom.PasswordEntryPopover passwordEntryPopover = null; + DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null; AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); - AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); @@ -83,12 +82,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithPasswordViaKeyboardOnly() { - DrawableRoom.PasswordEntryPopover passwordEntryPopover = null; + DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null; AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); - AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press enter", () => InputManager.Key(Key.Enter)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index ea10fc1b8b..21364fe154 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -59,23 +59,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for load", () => screen.IsCurrentScreen()); } - [Test] - public void TestSettingValidity() - { - AddAssert("create button not enabled", () => !this.ChildrenOfType().Single().Enabled.Value); - - AddStep("set playlist", () => - { - SelectedRoom.Value.Playlist.Add(new PlaylistItem - { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - }); - }); - - AddAssert("create button enabled", () => this.ChildrenOfType().Single().Enabled.Value); - } - [Test] public void TestCreatedRoom() { @@ -97,6 +80,23 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => Client.Room != null); } + [Test] + public void TestSettingValidity() + { + AddAssert("create button not enabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("set playlist", () => + { + SelectedRoom.Value.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + }); + }); + + AddAssert("create button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + } + [Test] public void TestStartMatchWhileSpectating() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs new file mode 100644 index 0000000000..80da7a7e5e --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.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 System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Multiplayer; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerPlayer : MultiplayerTestScene + { + private MultiplayerPlayer player; + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("set beatmap", () => + { + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + }); + + AddStep("initialise gameplay", () => + { + Stack.Push(player = new MultiplayerPlayer(Client.CurrentMatchPlayingItem.Value, Client.Room?.Users.ToArray())); + }); + } + + [Test] + public void TestGameplay() + { + AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value); + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs deleted file mode 100644 index b17427a30b..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs +++ /dev/null @@ -1,157 +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 NUnit.Framework; -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 : MultiplayerTestScene - { - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); - - public TestSceneMultiplayerRoomManager() - : base(false) - { - } - - [Test] - public void TestPollsInitially() - { - AddStep("create room manager with a few rooms", () => - { - 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); - } - - [Test] - public void TestRoomsClearedOnDisconnection() - { - AddStep("create room manager with a few rooms", () => - { - RoomManager.CreateRoom(createRoom()); - RoomManager.PartRoom(); - RoomManager.CreateRoom(createRoom()); - RoomManager.PartRoom(); - }); - - AddStep("disconnect", () => Client.Disconnect()); - - AddAssert("rooms cleared", () => ((RoomManager)RoomManager).Rooms.Count == 0); - AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value); - } - - [Test] - public void TestRoomsPolledOnReconnect() - { - AddStep("create room manager with a few rooms", () => - { - RoomManager.CreateRoom(createRoom()); - RoomManager.PartRoom(); - RoomManager.CreateRoom(createRoom()); - RoomManager.PartRoom(); - }); - - 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); - } - - [Test] - public void TestRoomsNotPolledWhenJoined() - { - AddStep("create room manager with a room", () => - { - RoomManager.CreateRoom(createRoom()); - RoomManager.ClearRooms(); - }); - - AddAssert("manager not polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 0); - AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value); - } - - [Test] - public void TestMultiplayerRoomJoinedWhenCreated() - { - AddStep("create room manager with a room", () => - { - RoomManager.CreateRoom(createRoom()); - }); - - AddUntilStep("multiplayer room joined", () => Client.Room != null); - } - - [Test] - public void TestMultiplayerRoomPartedWhenAPIRoomParted() - { - AddStep("create room manager with a room", () => - { - RoomManager.CreateRoom(createRoom()); - RoomManager.PartRoom(); - }); - - AddAssert("multiplayer room parted", () => Client.Room == null); - } - - [Test] - public void TestMultiplayerRoomJoinedWhenAPIRoomJoined() - { - AddStep("create room manager with a room", () => - { - var r = createRoom(); - RoomManager.CreateRoom(r); - RoomManager.PartRoom(); - RoomManager.JoinRoom(r); - }); - - AddUntilStep("multiplayer room joined", () => Client.Room != null); - } - - private Room createRoom(Action initFunc = null) - { - var room = new Room - { - Name = - { - Value = "test room" - }, - Playlist = - { - new PlaylistItem - { - Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value } - } - } - }; - - initFunc?.Invoke(room); - return room; - } - - private class TestDependencies : MultiplayerTestSceneDependencies - { - public TestDependencies() - { - // Need to set these values as early as possible. - RoomManager.TimeBetweenListingPolls.Value = 1; - RoomManager.TimeBetweenSelectionPolls.Value = 1; - } - } - } -} diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index 26641214b1..b8232837b5 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -88,21 +89,27 @@ namespace osu.Game.Tests.Visual.Navigation [Resolved] private OsuGameBase gameBase { get; set; } - [BackgroundDependencyLoader] - private void load(GameHost host) - { - game = new OsuGame(); - game.SetHost(host); + [Resolved] + private GameHost host { get; set; } - Children = new Drawable[] + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create game", () => { - new Box + game = new OsuGame(); + game.SetHost(host); + + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - game - }; + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + game + }; + }); AddUntilStep("wait for load", () => game.IsLoaded); } diff --git a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs index c1c968e862..7e3d8290be 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs @@ -9,9 +9,12 @@ namespace osu.Game.Tests.Visual.Navigation { public class TestSettingsMigration : OsuGameTestScene { - public override void RecycleLocalStorage() + public override void RecycleLocalStorage(bool isDisposing) { - base.RecycleLocalStorage(); + base.RecycleLocalStorage(isDisposing); + + if (isDisposing) + return; using (var config = new OsuConfigManager(LocalStorage)) { diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index aff0e7ba4b..a4bcb4baae 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Playlists { public class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene { - protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager; + protected new TestRequestHandlingRoomManager RoomManager => (TestRequestHandlingRoomManager)base.RoomManager; private LoungeSubScreen loungeScreen; @@ -37,6 +37,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("add rooms", () => RoomManager.AddRooms(30)); + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -53,6 +54,7 @@ namespace osu.Game.Tests.Visual.Playlists public void TestScrollSelectedIntoView() { AddStep("add rooms", () => RoomManager.AddRooms(30)); + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index cdc655500d..04b44efd32 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("error not displayed", () => !settings.ErrorText.IsPresent); } - private class TestRoomSettings : PlaylistsMatchSettingsOverlay + private class TestRoomSettings : PlaylistsRoomSettingsOverlay { public TriangleButton ApplyButton => ((MatchSettings)Settings).ApplyButton; @@ -141,6 +141,12 @@ namespace osu.Game.Tests.Visual.Playlists public IBindableList Rooms => null; + public void AddOrUpdateRoom(Room room) => throw new NotImplementedException(); + + public void RemoveRoom(Room room) => throw new NotImplementedException(); + + public void ClearRooms() => throw new NotImplementedException(); + public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) { if (CreateRequested == null) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 9fc29049ef..9051c71fc6 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -19,7 +18,6 @@ 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 @@ -36,18 +34,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)); - - ((DummyAPIAccess)API).HandleRequest = req => - { - switch (req) - { - case CreateRoomScoreRequest createRoomScoreRequest: - createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 }); - return true; - } - - return false; - }; } [SetUpSteps] @@ -66,7 +52,7 @@ namespace osu.Game.Tests.Visual.Playlists { SelectedRoom.Value.RoomID.Value = 1; SelectedRoom.Value.Name.Value = "my awesome room"; - SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" }; + SelectedRoom.Value.Host.Value = API.LocalUser.Value; SelectedRoom.Value.RecentParticipants.Add(SelectedRoom.Value.Host.Value); SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); SelectedRoom.Value.Playlist.Add(new PlaylistItem @@ -86,7 +72,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("set room properties", () => { SelectedRoom.Value.Name.Value = "my awesome room"; - SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" }; + SelectedRoom.Value.Host.Value = API.LocalUser.Value; SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, @@ -96,7 +82,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("move mouse to create button", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); }); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -137,7 +123,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("load room", () => { SelectedRoom.Value.Name.Value = "my awesome room"; - SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" }; + SelectedRoom.Value.Host.Value = API.LocalUser.Value; SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedSet.Beatmaps[0] }, @@ -147,7 +133,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("create room", () => { - InputManager.MoveMouseTo(match.ChildrenOfType().Single()); + InputManager.MoveMouseTo(match.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs deleted file mode 100644 index 566452249f..0000000000 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs +++ /dev/null @@ -1,65 +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.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Utils; -using osu.Game.Beatmaps; -using osu.Game.Screens.Ranking.Expanded; -using osuTK; - -namespace osu.Game.Tests.Visual.Ranking -{ - public class TestSceneStarRatingDisplay : OsuTestScene - { - [Test] - public void TestDisplay() - { - AddStep("load displays", () => Child = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - ChildrenEnumerable = new[] - { - 1.23, - 2.34, - 3.45, - 4.56, - 5.67, - 6.78, - 10.11, - }.Select(starRating => new StarRatingDisplay(new StarDifficulty(starRating, 0)) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }) - }); - } - - [Test] - public void TestChangingStarRatingDisplay() - { - StarRatingDisplay starRating = null; - - AddStep("load display", () => Child = starRating = new StarRatingDisplay(new StarDifficulty(5.55, 1)) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(3f), - }); - - AddRepeatStep("set random value", () => - { - starRating.Current.Value = new StarDifficulty(RNG.NextDouble(0.0, 11.0), 1); - }, 10); - - AddSliderStep("set exact stars", 0.0, 11.0, 5.55, d => - { - if (starRating != null) - starRating.Current.Value = new StarDifficulty(d, 1); - }); - } - } -} diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index fa2c9ecdea..57ba051214 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Settings [SetUpSteps] public void SetUpSteps() { + AddUntilStep("wait for load", () => panel.ChildrenOfType().Any()); AddStep("Scroll to top", () => panel.ChildrenOfType().First().ScrollToTop()); AddWaitStep("wait for scroll", 5); } diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs index df59b9284b..d9cce69ee3 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -76,5 +76,23 @@ namespace osu.Game.Tests.Visual.Settings AddStep("restore default", () => sliderBar.Current.SetDefault()); AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); } + + [Test] + public void TestWarningTextVisibility() + { + SettingsNumberBox numberBox = null; + + AddStep("create settings item", () => Child = numberBox = new SettingsNumberBox()); + AddAssert("warning text not created", () => !numberBox.ChildrenOfType().Any()); + + AddStep("set warning text", () => numberBox.WarningText = "this is a warning!"); + AddAssert("warning text created", () => numberBox.ChildrenOfType().Single().Alpha == 1); + + AddStep("unset warning text", () => numberBox.WarningText = default); + AddAssert("warning text hidden", () => numberBox.ChildrenOfType().Single().Alpha == 0); + + AddStep("set warning text again", () => numberBox.WarningText = "another warning!"); + AddAssert("warning text shown again", () => numberBox.ChildrenOfType().Single().Alpha == 1); + } } } diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index 115d2fec7d..0af77b3b5a 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Overlays; namespace osu.Game.Tests.Visual.Settings @@ -11,27 +12,39 @@ namespace osu.Game.Tests.Visual.Settings [TestFixture] public class TestSceneSettingsPanel : OsuTestScene { - private readonly SettingsPanel settings; - private readonly DialogOverlay dialogOverlay; + private SettingsPanel settings; + private DialogOverlay dialogOverlay; - public TestSceneSettingsPanel() + [SetUpSteps] + public void SetUpSteps() { - settings = new SettingsOverlay + AddStep("create settings", () => { - State = { Value = Visibility.Visible } - }; - Add(dialogOverlay = new DialogOverlay - { - Depth = -1 + settings?.Expire(); + + Add(settings = new SettingsOverlay + { + State = { Value = Visibility.Visible } + }); }); } + [Test] + public void ToggleVisibility() + { + AddWaitStep("wait some", 5); + AddToggleStep("toggle editor visibility", visible => settings.ToggleVisibility()); + } + [BackgroundDependencyLoader] private void load() { - Dependencies.Cache(dialogOverlay); + Add(dialogOverlay = new DialogOverlay + { + Depth = -1 + }); - Add(settings); + Dependencies.Cache(dialogOverlay); } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index a416fd4daf..2b38c4f936 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; @@ -65,6 +66,12 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("show", () => { infoWedge.Show(); }); + AddSliderStep("change star difficulty", 0, 11.9, 5.55, v => + { + foreach (var hasCurrentValue in infoWedge.Info.ChildrenOfType>()) + hasCurrentValue.Current.Value = new StarDifficulty(v, 0); + }); + foreach (var rulesetInfo in rulesets.AvailableRulesets) { var instance = rulesetInfo.CreateInstance(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 5e2d5eba5d..53cb628bb3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; -using osu.Game.Tests.Visual.Navigation; using osu.Game.Users; namespace osu.Game.Tests.Visual.SongSelect diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs index e0d76b3e4a..f8652573f4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs @@ -1,8 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -174,6 +176,60 @@ namespace osu.Game.Tests.Visual.UserInterface checkBindableAtValue("Circle Size", null); } + [Test] + public void TestModSettingChangeTracker() + { + ModSettingChangeTracker tracker = null; + Queue settingsChangedQueue = null; + + setBeatmapWithDifficultyParameters(5); + + AddStep("add mod settings change tracker", () => + { + settingsChangedQueue = new Queue(); + + tracker = new ModSettingChangeTracker(modDifficultyAdjust.Yield()) + { + SettingChanged = settingsChangedQueue.Enqueue + }; + }); + + AddAssert("no settings changed", () => settingsChangedQueue.Count == 0); + + setSliderValue("Circle Size", 3); + + settingsChangedFired(); + + setSliderValue("Circle Size", 5); + checkBindableAtValue("Circle Size", 5); + + settingsChangedFired(); + + AddStep("reset mod settings", () => modDifficultyAdjust.CircleSize.SetDefault()); + checkBindableAtValue("Circle Size", null); + + settingsChangedFired(); + + setExtendedLimits(true); + + settingsChangedFired(); + + AddStep("dispose tracker", () => + { + tracker.Dispose(); + tracker = null; + }); + + void settingsChangedFired() + { + AddAssert("setting changed event fired", () => + { + settingsChangedQueue.Dequeue(); + return settingsChangedQueue.Count == 0; + }); + } + } + private void resetToDefault(string name) { AddStep($"Reset {name} to default", () => diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs index 5c2e6e457d..2312c57af2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Graphics.Containers; using osuTK.Graphics; @@ -61,10 +62,12 @@ namespace osu.Game.Tests.Visual.UserInterface )); AddStep("scroll up", () => triggerUserScroll(1)); AddStep("scroll down", () => triggerUserScroll(-1)); + AddStep("scroll up a bit", () => triggerUserScroll(0.1f)); + AddStep("scroll down a bit", () => triggerUserScroll(-0.1f)); } [Test] - public void TestCorrectSectionSelected() + public void TestCorrectSelectionAndVisibleTop() { const int sections_count = 11; float[] alternating = { 0.07f, 0.33f, 0.16f, 0.33f }; @@ -79,6 +82,12 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep($"scroll to section {scrollIndex + 1}", () => container.ScrollTo(container.Children[scrollIndex])); AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[scrollIndex]); + AddUntilStep("section top is visible", () => + { + float scrollPosition = container.ChildrenOfType().First().Current; + float sectionTop = container.Children[scrollIndex].BoundingBox.Top; + return scrollPosition < sectionTop; + }); } for (int i = 1; i < sections_count; i++) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneStarRatingDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneStarRatingDisplay.cs new file mode 100644 index 0000000000..2806e6d347 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneStarRatingDisplay.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneStarRatingDisplay : OsuTestScene + { + [TestCase(StarRatingDisplaySize.Regular)] + [TestCase(StarRatingDisplaySize.Small)] + public void TestDisplay(StarRatingDisplaySize size) + { + AddStep("load displays", () => + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(2f), + Direction = FillDirection.Horizontal, + ChildrenEnumerable = Enumerable.Range(0, 15).Select(i => new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(2f), + Direction = FillDirection.Vertical, + ChildrenEnumerable = Enumerable.Range(0, 10).Select(j => new StarRatingDisplay(new StarDifficulty(i * (i >= 11 ? 25f : 1f) + j * 0.1f, 0), size) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }), + }) + }; + }); + } + + [Test] + public void TestSpectrum() + { + StarRatingDisplay starRating = null; + + AddStep("load display", () => Child = starRating = new StarRatingDisplay(new StarDifficulty(5.55, 1), animated: true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(3f), + }); + + AddRepeatStep("set random value", () => + { + starRating.Current.Value = new StarDifficulty(RNG.NextDouble(0.0, 11.0), 1); + }, 10); + + AddSliderStep("set exact stars", 0.0, 11.0, 5.55, d => + { + if (starRating != null) + starRating.Current.Value = new StarDifficulty(d, 1); + }); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 6ed623d0c0..5025e4ad4b 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -14,6 +14,7 @@ using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Threading; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -56,12 +57,28 @@ namespace osu.Game.Beatmaps [Resolved] private Bindable> currentMods { get; set; } + private ModSettingChangeTracker modSettingChangeTracker; + private ScheduledDelegate debouncedModSettingsChange; + protected override void LoadComplete() { base.LoadComplete(); currentRuleset.BindValueChanged(_ => updateTrackedBindables()); - currentMods.BindValueChanged(_ => updateTrackedBindables(), true); + + currentMods.BindValueChanged(mods => + { + modSettingChangeTracker?.Dispose(); + + updateTrackedBindables(); + + modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); + modSettingChangeTracker.SettingChanged += _ => + { + debouncedModSettingsChange?.Cancel(); + debouncedModSettingsChange = Scheduler.AddDelayed(updateTrackedBindables, 100); + }; + }, true); } /// @@ -84,7 +101,7 @@ namespace osu.Game.Beatmaps /// Retrieves a bindable containing the star difficulty of a with a given and combination. /// /// - /// The bindable will not update to follow the currently-selected ruleset and mods. + /// The bindable will not update to follow the currently-selected ruleset and mods or its settings. /// /// The to get the difficulty of. /// The to get the difficulty with. If null, the 's ruleset is used. @@ -275,6 +292,8 @@ namespace osu.Game.Beatmaps { base.Dispose(isDisposing); + modSettingChangeTracker?.Dispose(); + cancelTrackedBindableUpdate(); updateScheduler?.Dispose(); } @@ -297,7 +316,7 @@ namespace osu.Game.Beatmaps public bool Equals(DifficultyCacheLookup other) => Beatmap.ID == other.Beatmap.ID && Ruleset.ID == other.Ruleset.ID - && OrderedMods.Select(m => m.Acronym).SequenceEqual(other.OrderedMods.Select(m => m.Acronym)); + && OrderedMods.SequenceEqual(other.OrderedMods); public override int GetHashCode() { @@ -307,7 +326,7 @@ namespace osu.Game.Beatmaps hashCode.Add(Ruleset.ID); foreach (var mod in OrderedMods) - hashCode.Add(mod.Acronym); + hashCode.Add(mod); return hashCode.ToHashCode(); } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4a78ceb299..241649062e 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -365,6 +365,10 @@ namespace osu.Game.Beatmaps queryable = beatmaps.BeatmapSetsOverview; break; + case IncludedDetails.AllButRuleset: + queryable = beatmaps.BeatmapSetsWithoutRuleset; + break; + case IncludedDetails.AllButFiles: queryable = beatmaps.BeatmapSetsWithoutFiles; break; @@ -384,8 +388,33 @@ namespace osu.Game.Beatmaps /// Perform a lookup query on available s. /// /// The query. + /// The level of detail to include in the returned objects. /// Results from the provided query. - public IEnumerable QueryBeatmapSets(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().Where(query); + public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) + { + IQueryable queryable; + + switch (includes) + { + case IncludedDetails.Minimal: + queryable = beatmaps.BeatmapSetsOverview; + break; + + case IncludedDetails.AllButRuleset: + queryable = beatmaps.BeatmapSetsWithoutRuleset; + break; + + case IncludedDetails.AllButFiles: + queryable = beatmaps.BeatmapSetsWithoutFiles; + break; + + default: + queryable = beatmaps.ConsumableItems; + break; + } + + return queryable.AsNoTracking().Where(query); + } /// /// Perform a lookup query on available s. @@ -554,6 +583,11 @@ namespace osu.Game.Beatmaps /// AllButFiles, + /// + /// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap. + /// + AllButRuleset, + /// /// Include everything. /// diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 642bafd2ac..e3214b7c03 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -92,6 +92,13 @@ namespace osu.Game.Beatmaps .Include(s => s.Beatmaps) .AsNoTracking(); + public IQueryable BeatmapSetsWithoutRuleset => ContextFactory.Get().BeatmapSetInfo + .Include(s => s.Metadata) + .Include(s => s.Files).ThenInclude(f => f.FileInfo) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .AsNoTracking(); + public IQueryable BeatmapSetsWithoutFiles => ContextFactory.Get().BeatmapSetInfo .Include(s => s.Metadata) .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset) diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs new file mode 100644 index 0000000000..c239fda455 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs @@ -0,0 +1,168 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Beatmaps.Drawables +{ + /// + /// A pill that displays the star rating of a beatmap. + /// + public class StarRatingDisplay : CompositeDrawable, IHasCurrentValue + { + private readonly bool animated; + private readonly Box background; + private readonly SpriteIcon starIcon; + private readonly OsuSpriteText starsText; + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly Bindable displayedStars = new BindableDouble(); + + /// + /// The currently displayed stars of this display wrapped in a bindable. + /// This bindable gets transformed on change rather than instantaneous, if animation is enabled. + /// + public IBindable DisplayedStars => displayedStars; + + [Resolved] + private OsuColour colours { get; set; } + + [Resolved(canBeNull: true)] + private OverlayColourProvider colourProvider { get; set; } + + /// + /// Creates a new using an already computed . + /// + /// The already computed to display. + /// The size of the star rating display. + /// Whether the star rating display will perform transforms on change rather than updating instantaneously. + public StarRatingDisplay(StarDifficulty starDifficulty, StarRatingDisplaySize size = StarRatingDisplaySize.Regular, bool animated = false) + { + this.animated = animated; + + Current.Value = starDifficulty; + + AutoSizeAxes = Axes.Both; + + MarginPadding margin = default; + + switch (size) + { + case StarRatingDisplaySize.Small: + margin = new MarginPadding { Horizontal = 7f }; + break; + + case StarRatingDisplaySize.Range: + margin = new MarginPadding { Horizontal = 8f }; + break; + + case StarRatingDisplaySize.Regular: + margin = new MarginPadding { Horizontal = 8f, Vertical = 2f }; + break; + } + + InternalChild = new CircularContainer + { + Masking = true, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Margin = margin, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 3f), + new Dimension(GridSizeMode.AutoSize, minSize: 25f), + }, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + Content = new[] + { + new[] + { + starIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Star, + Size = new Vector2(8f), + }, + Empty(), + starsText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Bottom = 1.5f }, + // todo: this should be size: 12f, but to match up with the design, it needs to be 14.4f + // see https://github.com/ppy/osu-framework/issues/3271. + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + Shadow = false, + }, + } + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(c => + { + if (animated) + this.TransformBindableTo(displayedStars, c.NewValue.Stars, 750, Easing.OutQuint); + else + displayedStars.Value = c.NewValue.Stars; + }); + + displayedStars.Value = Current.Value.Stars; + + displayedStars.BindValueChanged(s => + { + starsText.Text = s.NewValue.ToLocalisableString("0.00"); + + background.Colour = colours.ForStarDifficulty(s.NewValue); + + starIcon.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47"); + starsText.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f); + }, true); + } + } + + public enum StarRatingDisplaySize + { + Small, + Range, + Regular, + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 6c7adcc806..9b0d7f51da 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -101,7 +101,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.HitLighting, true); SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always); - SetDefault(OsuSetting.ShowDifficultyGraph, true); + SetDefault(OsuSetting.ShowProgressGraph, true); SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); SetDefault(OsuSetting.KeyOverlay, false); @@ -201,6 +201,8 @@ namespace osu.Game.Configuration public Func LookupKeyBindings { get; set; } } + // IMPORTANT: These are used in user configuration files. + // The naming of these keys should not be changed once they are deployed in a release, unless migration logic is also added. public enum OsuSetting { Ruleset, @@ -217,7 +219,7 @@ namespace osu.Game.Configuration AlwaysPlayFirstComboBreak, FloatingComments, HUDVisibilityMode, - ShowDifficultyGraph, + ShowProgressGraph, ShowHealthDisplayWhenCantFail, FadePlayfieldWhenHealthLow, MouseDisableButtons, diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs index c9d0c4bc41..b0feb7bb78 100644 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -36,6 +36,11 @@ namespace osu.Game.Database /// public IQueryable ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set()); + /// + /// Access barebones items with no includes. + /// + public IQueryable Items => ContextFactory.Get().Set(); + /// /// Add a to the database. /// diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 8ab146efe7..76492cab55 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -22,7 +22,8 @@ namespace osu.Game.Graphics.Containers where T : Drawable { public Bindable SelectedSection { get; } = new Bindable(); - private Drawable lastClickedSection; + + private T lastClickedSection; public Drawable ExpandableHeader { @@ -144,10 +145,25 @@ namespace osu.Game.Graphics.Containers footerHeight = null; } - public void ScrollTo(Drawable section) + public void ScrollTo(Drawable target) { - lastClickedSection = section; - scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - scrollContainer.DisplayableContent * scroll_y_centre - (FixedHeader?.BoundingBox.Height ?? 0)); + lastKnownScroll = null; + + float fixedHeaderSize = FixedHeader?.BoundingBox.Height ?? 0; + + // implementation similar to ScrollIntoView but a bit more nuanced. + float top = scrollContainer.GetChildPosInContent(target); + + float bottomScrollExtent = scrollContainer.ScrollableExtent - fixedHeaderSize; + float scrollTarget = top - fixedHeaderSize - scrollContainer.DisplayableContent * scroll_y_centre; + + if (scrollTarget > bottomScrollExtent) + scrollContainer.ScrollToEnd(); + else + scrollContainer.ScrollTo(scrollTarget); + + if (target is T section) + lastClickedSection = section; } public void ScrollToTop() => scrollContainer.ScrollTo(0); @@ -170,13 +186,22 @@ namespace osu.Game.Graphics.Containers if (source == InvalidationSource.Child && (invalidation & Invalidation.DrawSize) != 0) { - lastKnownScroll = null; + InvalidateScrollPosition(); result = true; } return result; } + protected void InvalidateScrollPosition() + { + Schedule(() => + { + lastKnownScroll = null; + lastClickedSection = null; + }); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -224,15 +249,19 @@ namespace osu.Game.Graphics.Containers float scrollCentre = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_y_centre + selectionLenienceAboveSection; - if (Precision.AlmostBigger(0, scrollContainer.Current)) - SelectedSection.Value = lastClickedSection as T ?? Children.FirstOrDefault(); + var presentChildren = Children.Where(c => c.IsPresent); + + if (lastClickedSection != null) + SelectedSection.Value = lastClickedSection; + else if (Precision.AlmostBigger(0, scrollContainer.Current)) + SelectedSection.Value = presentChildren.FirstOrDefault(); else if (Precision.AlmostBigger(scrollContainer.Current, scrollContainer.ScrollableExtent)) - SelectedSection.Value = lastClickedSection as T ?? Children.LastOrDefault(); + SelectedSection.Value = presentChildren.LastOrDefault(); else { - SelectedSection.Value = Children + SelectedSection.Value = presentChildren .TakeWhile(section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollCentre <= 0) - .LastOrDefault() ?? Children.FirstOrDefault(); + .LastOrDefault() ?? presentChildren.FirstOrDefault(); } } } diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs index 17506ce0f5..0561051e35 100644 --- a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs @@ -42,6 +42,12 @@ namespace osu.Game.Graphics.Containers base.OnUserScroll(value, animated, distanceDecay); } + public new void ScrollIntoView(Drawable target, bool animated = true) + { + UserScrolling = false; + base.ScrollIntoView(target, animated); + } + public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null) { UserScrolling = false; diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index f77a3109c9..ed9f0710b0 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -22,9 +22,14 @@ namespace osu.Game.Graphics.UserInterface public void TakeFocus() { - if (allowImmediateFocus) GetContainingInputManager().ChangeFocus(this); + if (!allowImmediateFocus) + return; + + Scheduler.Add(() => GetContainingInputManager().ChangeFocus(this), false); } + public new void KillFocus() => base.KillFocus(); + public bool HoldFocus { get => allowImmediateFocus && focus; diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs index 8d686e8c2f..664f32b083 100644 --- a/osu.Game/Graphics/UserInterface/Nub.cs +++ b/osu.Game/Graphics/UserInterface/Nub.cs @@ -6,6 +6,7 @@ using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -57,18 +58,13 @@ namespace osu.Game.Graphics.UserInterface EdgeEffect = new EdgeEffectParameters { - Colour = GlowColour, + Colour = GlowColour.Opacity(0), Type = EdgeEffectType.Glow, Radius = 10, Roundness = 8, }; } - protected override void LoadComplete() - { - FadeEdgeEffectTo(0); - } - private bool glowing; public bool Glowing @@ -153,7 +149,7 @@ namespace osu.Game.Graphics.UserInterface glowColour = value; var effect = EdgeEffect; - effect.Colour = value; + effect.Colour = Glowing ? value : value.Opacity(0); EdgeEffect = effect; } } diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index cd9ca9f87f..82a3e73b84 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -36,6 +36,7 @@ namespace osu.Game.Graphics.UserInterface public Color4 BackgroundColour { + get => backgroundColour ?? Color4.White; set { backgroundColour = value; diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 61dd5fb2d9..42f628a75a 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -69,6 +69,7 @@ namespace osu.Game.Graphics.UserInterface BackgroundColour = Color4.Black.Opacity(0.5f); MaskingContainer.CornerRadius = corner_radius; + Alpha = 0; // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring ItemsContainer.Padding = new MarginPadding(5); @@ -94,9 +95,11 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateClose() { - this.FadeOut(300, Easing.OutQuint); if (wasOpened) + { + this.FadeOut(300, Easing.OutQuint); sampleClose?.Play(); + } } // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index d3cc90ef99..0176a00e9d 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -90,6 +90,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward), new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), + new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus), }; public IEnumerable SongSelectKeyBindings => new[] @@ -280,5 +281,8 @@ namespace osu.Game.Input.Bindings [Description("Seek replay backward")] SeekReplayBackward, + + [Description("Toggle chat focus")] + ToggleChatFocus } } diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index 75d9c8debb..d2bf953dbc 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -7,6 +7,7 @@ using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Game.Configuration; +using osu.Game.Screens.Play; namespace osu.Game.Input { @@ -24,14 +25,14 @@ namespace osu.Game.Input private IBindable localUserPlaying; [BackgroundDependencyLoader] - private void load(OsuGame game, FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager) + private void load(ILocalUserPlayInfo localUserInfo, FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager) { frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode); frameworkWindowMode = frameworkConfigManager.GetBindable(FrameworkSetting.WindowMode); frameworkWindowMode.BindValueChanged(_ => updateConfineMode()); osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode); - localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); + localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy(); osuConfineMode.ValueChanged += _ => updateConfineMode(); localUserPlaying.BindValueChanged(_ => updateConfineMode(), true); diff --git a/osu.Game/Localisation/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs new file mode 100644 index 0000000000..aa6eabd7d1 --- /dev/null +++ b/osu.Game/Localisation/AudioSettingsStrings.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class AudioSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.AudioSettings"; + + /// + /// "Audio" + /// + public static LocalisableString AudioSectionHeader => new TranslatableString(getKey(@"audio_section_header"), @"Audio"); + + /// + /// "Devices" + /// + public static LocalisableString AudioDevicesHeader => new TranslatableString(getKey(@"audio_devices_header"), @"Devices"); + + /// + /// "Volume" + /// + public static LocalisableString VolumeHeader => new TranslatableString(getKey(@"volume_header"), @"Volume"); + + /// + /// "Master" + /// + public static LocalisableString MasterVolume => new TranslatableString(getKey(@"master_volume"), @"Master"); + + /// + /// "Master (window inactive)" + /// + public static LocalisableString MasterVolumeInactive => new TranslatableString(getKey(@"master_volume_inactive"), @"Master (window inactive)"); + + /// + /// "Effect" + /// + public static LocalisableString EffectVolume => new TranslatableString(getKey(@"effect_volume"), @"Effect"); + + /// + /// "Music" + /// + public static LocalisableString MusicVolume => new TranslatableString(getKey(@"music_volume"), @"Music"); + + /// + /// "Offset Adjustment" + /// + public static LocalisableString OffsetHeader => new TranslatableString(getKey(@"offset_header"), @"Offset Adjustment"); + + /// + /// "Audio offset" + /// + public static LocalisableString AudioOffset => new TranslatableString(getKey(@"audio_offset"), @"Audio offset"); + + /// + /// "Offset wizard" + /// + public static LocalisableString OffsetWizard => new TranslatableString(getKey(@"offset_wizard"), @"Offset wizard"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index b4381fc442..33a6eb5d58 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -14,11 +14,21 @@ namespace osu.Game.Localisation /// public static LocalisableString Cancel => new TranslatableString(getKey(@"cancel"), @"Cancel"); + /// + /// "Clear" + /// + public static LocalisableString Clear => new TranslatableString(getKey(@"clear"), @"Clear"); + /// /// "Enabled" /// public static LocalisableString Enabled => new TranslatableString(getKey(@"enabled"), @"Enabled"); + /// + /// "Default" + /// + public static LocalisableString Default => new TranslatableString(getKey(@"default"), @"Default"); + /// /// "Width" /// diff --git a/osu.Game/Localisation/DebugSettingsStrings.cs b/osu.Game/Localisation/DebugSettingsStrings.cs new file mode 100644 index 0000000000..dd21739096 --- /dev/null +++ b/osu.Game/Localisation/DebugSettingsStrings.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.Localisation; + +namespace osu.Game.Localisation +{ + public static class DebugSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.DebugSettings"; + + /// + /// "Debug" + /// + public static LocalisableString DebugSectionHeader => new TranslatableString(getKey(@"debug_section_header"), @"Debug"); + + /// + /// "General" + /// + public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General"); + + /// + /// "Show log overlay" + /// + public static LocalisableString ShowLogOverlay => new TranslatableString(getKey(@"show_log_overlay"), @"Show log overlay"); + + /// + /// "Bypass front-to-back render pass" + /// + public static LocalisableString BypassFrontToBackPass => new TranslatableString(getKey(@"bypass_front_to_back_pass"), @"Bypass front-to-back render pass"); + + /// + /// "Import files" + /// + public static LocalisableString ImportFiles => new TranslatableString(getKey(@"import_files"), @"Import files"); + + /// + /// "Memory" + /// + public static LocalisableString MemoryHeader => new TranslatableString(getKey(@"memory_header"), @"Memory"); + + /// + /// "Clear all caches" + /// + public static LocalisableString ClearAllCaches => new TranslatableString(getKey(@"clear_all_caches"), @"Clear all caches"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs new file mode 100644 index 0000000000..6d6381b429 --- /dev/null +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class GameplaySettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.GameplaySettings"; + + /// + /// "Gameplay" + /// + public static LocalisableString GameplaySectionHeader => new TranslatableString(getKey(@"gameplay_section_header"), @"Gameplay"); + + /// + /// "General" + /// + public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General"); + + /// + /// "Background dim" + /// + public static LocalisableString BackgroundDim => new TranslatableString(getKey(@"dim"), @"Background dim"); + + /// + /// "Background blur" + /// + public static LocalisableString BackgroundBlur => new TranslatableString(getKey(@"blur"), @"Background blur"); + + /// + /// "Lighten playfield during breaks" + /// + public static LocalisableString LightenDuringBreaks => new TranslatableString(getKey(@"lighten_during_breaks"), @"Lighten playfield during breaks"); + + /// + /// "HUD overlay visibility mode" + /// + public static LocalisableString HUDVisibilityMode => new TranslatableString(getKey(@"hud_visibility_mode"), @"HUD overlay visibility mode"); + + /// + /// "Show difficulty graph on progress bar" + /// + public static LocalisableString ShowDifficultyGraph => new TranslatableString(getKey(@"show_difficulty_graph"), @"Show difficulty graph on progress bar"); + + /// + /// "Show health display even when you can't fail" + /// + public static LocalisableString ShowHealthDisplayWhenCantFail => new TranslatableString(getKey(@"show_health_display_when_cant_fail"), @"Show health display even when you can't fail"); + + /// + /// "Fade playfield to red when health is low" + /// + public static LocalisableString FadePlayfieldWhenHealthLow => new TranslatableString(getKey(@"fade_playfield_when_health_low"), @"Fade playfield to red when health is low"); + + /// + /// "Always show key overlay" + /// + public static LocalisableString AlwaysShowKeyOverlay => new TranslatableString(getKey(@"key_overlay"), @"Always show key overlay"); + + /// + /// "Positional hitsounds" + /// + public static LocalisableString PositionalHitsounds => new TranslatableString(getKey(@"positional_hitsounds"), @"Positional hitsounds"); + + /// + /// "Always play first combo break sound" + /// + public static LocalisableString AlwaysPlayFirstComboBreak => new TranslatableString(getKey(@"always_play_first_combo_break"), @"Always play first combo break sound"); + + /// + /// "Score display mode" + /// + public static LocalisableString ScoreDisplayMode => new TranslatableString(getKey(@"score_display_mode"), @"Score display mode"); + + /// + /// "Disable Windows key during gameplay" + /// + public static LocalisableString DisableWinKey => new TranslatableString(getKey(@"disable_win_key"), @"Disable Windows key during gameplay"); + + /// + /// "Mods" + /// + public static LocalisableString ModsHeader => new TranslatableString(getKey(@"mods_header"), @"Mods"); + + /// + /// "Increase visibility of first object when visual impairment mods are enabled" + /// + public static LocalisableString IncreaseFirstObjectVisibility => new TranslatableString(getKey(@"increase_first_object_visibility"), @"Increase visibility of first object when visual impairment mods are enabled"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs new file mode 100644 index 0000000000..a60e4891f4 --- /dev/null +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class GeneralSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.GeneralSettings"; + + /// + /// "General" + /// + public static LocalisableString GeneralSectionHeader => new TranslatableString(getKey(@"general_section_header"), @"General"); + + /// + /// "Language" + /// + public static LocalisableString LanguageHeader => new TranslatableString(getKey(@"language_header"), @"Language"); + + /// + /// "Language" + /// + public static LocalisableString LanguageDropdown => new TranslatableString(getKey(@"language_dropdown"), @"Language"); + + /// + /// "Prefer metadata in original language" + /// + public static LocalisableString PreferOriginalMetadataLanguage => new TranslatableString(getKey(@"prefer_original"), @"Prefer metadata in original language"); + + /// + /// "Updates" + /// + public static LocalisableString UpdateHeader => new TranslatableString(getKey(@"update_header"), @"Updates"); + + /// + /// "Release stream" + /// + public static LocalisableString ReleaseStream => new TranslatableString(getKey(@"release_stream"), @"Release stream"); + + /// + /// "Check for updates" + /// + public static LocalisableString CheckUpdate => new TranslatableString(getKey(@"check_update"), @"Check for updates"); + + /// + /// "Open osu! folder" + /// + public static LocalisableString OpenOsuFolder => new TranslatableString(getKey(@"open_osu_folder"), @"Open osu! folder"); + + /// + /// "Change folder location..." + /// + public static LocalisableString ChangeFolderLocation => new TranslatableString(getKey(@"change_folder_location"), @"Change folder location..."); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs new file mode 100644 index 0000000000..0e384f983f --- /dev/null +++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs @@ -0,0 +1,119 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class GraphicsSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.GraphicsSettings"; + + /// + /// "Graphics" + /// + public static LocalisableString GraphicsSectionHeader => new TranslatableString(getKey(@"graphics_section_header"), @"Graphics"); + + /// + /// "Renderer" + /// + public static LocalisableString RendererHeader => new TranslatableString(getKey(@"renderer_header"), @"Renderer"); + + /// + /// "Frame limiter" + /// + public static LocalisableString FrameLimiter => new TranslatableString(getKey(@"frame_limiter"), @"Frame limiter"); + + /// + /// "Threading mode" + /// + public static LocalisableString ThreadingMode => new TranslatableString(getKey(@"threading_mode"), @"Threading mode"); + + /// + /// "Show FPS" + /// + public static LocalisableString ShowFPS => new TranslatableString(getKey(@"show_fps"), @"Show FPS"); + + /// + /// "Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. "2x refresh rate" is recommended." + /// + public static LocalisableString UnlimitedFramesNote => new TranslatableString(getKey(@"unlimited_frames_note"), @"Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. ""2x refresh rate"" is recommended."); + + /// + /// "Layout" + /// + public static LocalisableString LayoutHeader => new TranslatableString(getKey(@"layout_header"), @"Layout"); + + /// + /// "Screen mode" + /// + public static LocalisableString ScreenMode => new TranslatableString(getKey(@"screen_mode"), @"Screen mode"); + + /// + /// "Resolution" + /// + public static LocalisableString Resolution => new TranslatableString(getKey(@"resolution"), @"Resolution"); + + /// + /// "UI scaling" + /// + public static LocalisableString UIScaling => new TranslatableString(getKey(@"ui_scaling"), @"UI scaling"); + + /// + /// "Screen scaling" + /// + public static LocalisableString ScreenScaling => new TranslatableString(getKey(@"screen_scaling"), @"Screen scaling"); + + /// + /// "Horizontal position" + /// + public static LocalisableString HorizontalPosition => new TranslatableString(getKey(@"horizontal_position"), @"Horizontal position"); + + /// + /// "Vertical position" + /// + public static LocalisableString VerticalPosition => new TranslatableString(getKey(@"vertical_position"), @"Vertical position"); + + /// + /// "Horizontal scale" + /// + public static LocalisableString HorizontalScale => new TranslatableString(getKey(@"horizontal_scale"), @"Horizontal scale"); + + /// + /// "Vertical scale" + /// + public static LocalisableString VerticalScale => new TranslatableString(getKey(@"vertical_scale"), @"Vertical scale"); + + /// + /// "Running without fullscreen mode will increase your input latency!" + /// + public static LocalisableString NotFullscreenNote => new TranslatableString(getKey(@"not_fullscreen_note"), @"Running without fullscreen mode will increase your input latency!"); + + /// + /// "Detail Settings" + /// + public static LocalisableString DetailSettingsHeader => new TranslatableString(getKey(@"detail_settings_header"), @"Detail Settings"); + + /// + /// "Storyboard / video" + /// + public static LocalisableString StoryboardVideo => new TranslatableString(getKey(@"storyboard_video"), @"Storyboard / video"); + + /// + /// "Hit lighting" + /// + public static LocalisableString HitLighting => new TranslatableString(getKey(@"hit_lighting"), @"Hit lighting"); + + /// + /// "Screenshot format" + /// + public static LocalisableString ScreenshotFormat => new TranslatableString(getKey(@"screenshot_format"), @"Screenshot format"); + + /// + /// "Show menu cursor in screenshots" + /// + public static LocalisableString ShowCursorInScreenshots => new TranslatableString(getKey(@"show_cursor_in_screenshots"), @"Show menu cursor in screenshots"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/InputSettingsStrings.cs b/osu.Game/Localisation/InputSettingsStrings.cs new file mode 100644 index 0000000000..e46b4cecf3 --- /dev/null +++ b/osu.Game/Localisation/InputSettingsStrings.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class InputSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.InputSettings"; + + /// + /// "Input" + /// + public static LocalisableString InputSectionHeader => new TranslatableString(getKey(@"input_section_header"), @"Input"); + + /// + /// "Global" + /// + public static LocalisableString GlobalKeyBindingHeader => new TranslatableString(getKey(@"global_key_binding_header"), @"Global"); + + /// + /// "Song Select" + /// + public static LocalisableString SongSelectSection => new TranslatableString(getKey(@"song_select_section"), @"Song Select"); + + /// + /// "In Game" + /// + public static LocalisableString InGameSection => new TranslatableString(getKey(@"in_game_section"), @"In Game"); + + /// + /// "Audio" + /// + public static LocalisableString AudioSection => new TranslatableString(getKey(@"audio_section"), @"Audio"); + + /// + /// "Editor" + /// + public static LocalisableString EditorSection => new TranslatableString(getKey(@"editor_section"), @"Editor"); + + /// + /// "Reset all bindings in section" + /// + public static LocalisableString ResetSectionButton => new TranslatableString(getKey(@"reset_section_button"), @"Reset all bindings in section"); + + /// + /// "key configuration" + /// + public static LocalisableString KeyBindingPanelHeader => new TranslatableString(getKey(@"key_binding_panel_header"), @"key configuration"); + + /// + /// "Customise your keys!" + /// + public static LocalisableString KeyBindingPanelDescription => new TranslatableString(getKey(@"key_binding_panel_description"), @"Customise your keys!"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/MaintenanceSettingsStrings.cs b/osu.Game/Localisation/MaintenanceSettingsStrings.cs new file mode 100644 index 0000000000..a0e1a9ddab --- /dev/null +++ b/osu.Game/Localisation/MaintenanceSettingsStrings.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class MaintenanceSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.MaintenanceSettings"; + + /// + /// "Maintenance" + /// + public static LocalisableString MaintenanceSectionHeader => new TranslatableString(getKey(@"maintenance_section_header"), @"Maintenance"); + + /// + /// "Select directory" + /// + public static LocalisableString SelectDirectory => new TranslatableString(getKey(@"select_directory"), @"Select directory"); + + /// + /// "Import beatmaps from stable" + /// + public static LocalisableString ImportBeatmapsFromStable => new TranslatableString(getKey(@"import_beatmaps_from_stable"), @"Import beatmaps from stable"); + + /// + /// "Delete ALL beatmaps" + /// + public static LocalisableString DeleteAllBeatmaps => new TranslatableString(getKey(@"delete_all_beatmaps"), @"Delete ALL beatmaps"); + + /// + /// "Import scores from stable" + /// + public static LocalisableString ImportScoresFromStable => new TranslatableString(getKey(@"import_scores_from_stable"), @"Import scores from stable"); + + /// + /// "Delete ALL scores" + /// + public static LocalisableString DeleteAllScores => new TranslatableString(getKey(@"delete_all_scores"), @"Delete ALL scores"); + + /// + /// "Import skins from stable" + /// + public static LocalisableString ImportSkinsFromStable => new TranslatableString(getKey(@"import_skins_from_stable"), @"Import skins from stable"); + + /// + /// "Delete ALL skins" + /// + public static LocalisableString DeleteAllSkins => new TranslatableString(getKey(@"delete_all_skins"), @"Delete ALL skins"); + + /// + /// "Import collections from stable" + /// + public static LocalisableString ImportCollectionsFromStable => new TranslatableString(getKey(@"import_collections_from_stable"), @"Import collections from stable"); + + /// + /// "Delete ALL collections" + /// + public static LocalisableString DeleteAllCollections => new TranslatableString(getKey(@"delete_all_collections"), @"Delete ALL collections"); + + /// + /// "Restore all hidden difficulties" + /// + public static LocalisableString RestoreAllHiddenDifficulties => new TranslatableString(getKey(@"restore_all_hidden_difficulties"), @"Restore all hidden difficulties"); + + /// + /// "Restore all recently deleted beatmaps" + /// + public static LocalisableString RestoreAllRecentlyDeletedBeatmaps => new TranslatableString(getKey(@"restore_all_recently_deleted_beatmaps"), @"Restore all recently deleted beatmaps"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/OnlineSettingsStrings.cs b/osu.Game/Localisation/OnlineSettingsStrings.cs new file mode 100644 index 0000000000..6862f4ac2c --- /dev/null +++ b/osu.Game/Localisation/OnlineSettingsStrings.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class OnlineSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.OnlineSettings"; + + /// + /// "Online" + /// + public static LocalisableString OnlineSectionHeader => new TranslatableString(getKey(@"online_section_header"), @"Online"); + + /// + /// "Alerts and Privacy" + /// + public static LocalisableString AlertsAndPrivacyHeader => new TranslatableString(getKey(@"alerts_and_privacy_header"), @"Alerts and Privacy"); + + /// + /// "Show a notification when someone mentions your name" + /// + public static LocalisableString NotifyOnMentioned => new TranslatableString(getKey(@"notify_on_mentioned"), @"Show a notification when someone mentions your name"); + + /// + /// "Show a notification when you receive a private message" + /// + public static LocalisableString NotifyOnPrivateMessage => new TranslatableString(getKey(@"notify_on_private_message"), @"Show a notification when you receive a private message"); + + /// + /// "Integrations" + /// + public static LocalisableString IntegrationsHeader => new TranslatableString(getKey(@"integrations_header"), @"Integrations"); + + /// + /// "Discord Rich Presence" + /// + public static LocalisableString DiscordRichPresence => new TranslatableString(getKey(@"discord_rich_presence"), @"Discord Rich Presence"); + + /// + /// "Web" + /// + public static LocalisableString WebHeader => new TranslatableString(getKey(@"web_header"), @"Web"); + + /// + /// "Warn about opening external links" + /// + public static LocalisableString ExternalLinkWarning => new TranslatableString(getKey(@"external_link_warning"), @"Warn about opening external links"); + + /// + /// "Prefer downloads without video" + /// + public static LocalisableString PreferNoVideo => new TranslatableString(getKey(@"prefer_no_video"), @"Prefer downloads without video"); + + /// + /// "Automatically download beatmaps when spectating" + /// + public static LocalisableString AutomaticallyDownloadWhenSpectating => new TranslatableString(getKey(@"automatically_download_when_spectating"), @"Automatically download beatmaps when spectating"); + + /// + /// "Show explicit content in search results" + /// + public static LocalisableString ShowExplicitContent => new TranslatableString(getKey(@"show_explicit_content"), @"Show explicit content in search results"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs new file mode 100644 index 0000000000..f22b4d6bf5 --- /dev/null +++ b/osu.Game/Localisation/SkinSettingsStrings.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class SkinSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.SkinSettings"; + + /// + /// "Skin" + /// + public static LocalisableString SkinSectionHeader => new TranslatableString(getKey(@"skin_section_header"), @"Skin"); + + /// + /// "Skin layout editor" + /// + public static LocalisableString SkinLayoutEditor => new TranslatableString(getKey(@"skin_layout_editor"), @"Skin layout editor"); + + /// + /// "Gameplay cursor size" + /// + public static LocalisableString GameplayCursorSize => new TranslatableString(getKey(@"gameplay_cursor_size"), @"Gameplay cursor size"); + + /// + /// "Adjust gameplay cursor size based on current beatmap" + /// + public static LocalisableString AutoCursorSize => new TranslatableString(getKey(@"auto_cursor_size"), @"Adjust gameplay cursor size based on current beatmap"); + + /// + /// "Beatmap skins" + /// + public static LocalisableString BeatmapSkins => new TranslatableString(getKey(@"beatmap_skins"), @"Beatmap skins"); + + /// + /// "Beatmap colours" + /// + public static LocalisableString BeatmapColours => new TranslatableString(getKey(@"beatmap_colours"), @"Beatmap colours"); + + /// + /// "Beatmap hitsounds" + /// + public static LocalisableString BeatmapHitsounds => new TranslatableString(getKey(@"beatmap_hitsounds"), @"Beatmap hitsounds"); + + /// + /// "Export selected skin" + /// + public static LocalisableString ExportSkinButton => new TranslatableString(getKey(@"export_skin_button"), @"Export selected skin"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/UserInterfaceStrings.cs b/osu.Game/Localisation/UserInterfaceStrings.cs new file mode 100644 index 0000000000..4be403edb4 --- /dev/null +++ b/osu.Game/Localisation/UserInterfaceStrings.cs @@ -0,0 +1,114 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class UserInterfaceStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.UserInterface"; + + /// + /// "User Interface" + /// + public static LocalisableString UserInterfaceSectionHeader => new TranslatableString(getKey(@"user_interface_section_header"), @"User Interface"); + + /// + /// "General" + /// + public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General"); + + /// + /// "Rotate cursor when dragging" + /// + public static LocalisableString CursorRotation => new TranslatableString(getKey(@"cursor_rotation"), @"Rotate cursor when dragging"); + + /// + /// "Menu cursor size" + /// + public static LocalisableString MenuCursorSize => new TranslatableString(getKey(@"menu_cursor_size"), @"Menu cursor size"); + + /// + /// "Parallax" + /// + public static LocalisableString Parallax => new TranslatableString(getKey(@"parallax"), @"Parallax"); + + /// + /// "Hold-to-confirm activation time" + /// + public static LocalisableString HoldToConfirmActivationTime => new TranslatableString(getKey(@"hold_to_confirm_activation_time"), @"Hold-to-confirm activation time"); + + /// + /// "Main Menu" + /// + public static LocalisableString MainMenuHeader => new TranslatableString(getKey(@"main_menu_header"), @"Main Menu"); + + /// + /// "Interface voices" + /// + public static LocalisableString InterfaceVoices => new TranslatableString(getKey(@"interface_voices"), @"Interface voices"); + + /// + /// "osu! music theme" + /// + public static LocalisableString OsuMusicTheme => new TranslatableString(getKey(@"osu_music_theme"), @"osu! music theme"); + + /// + /// "Intro sequence" + /// + public static LocalisableString IntroSequence => new TranslatableString(getKey(@"intro_sequence"), @"Intro sequence"); + + /// + /// "Background source" + /// + public static LocalisableString BackgroundSource => new TranslatableString(getKey(@"background_source"), @"Background source"); + + /// + /// "Seasonal backgrounds" + /// + public static LocalisableString SeasonalBackgrounds => new TranslatableString(getKey(@"seasonal_backgrounds"), @"Seasonal backgrounds"); + + /// + /// "Changes to this setting will only apply with an active osu!supporter tag." + /// + public static LocalisableString NotSupporterNote => new TranslatableString(getKey(@"not_supporter_note"), @"Changes to this setting will only apply with an active osu!supporter tag."); + + /// + /// "Song Select" + /// + public static LocalisableString SongSelectHeader => new TranslatableString(getKey(@"song_select_header"), @"Song Select"); + + /// + /// "Right mouse drag to absolute scroll" + /// + public static LocalisableString RightMouseScroll => new TranslatableString(getKey(@"right_mouse_scroll"), @"Right mouse drag to absolute scroll"); + + /// + /// "Show converted beatmaps" + /// + public static LocalisableString ShowConvertedBeatmaps => new TranslatableString(getKey(@"show_converted_beatmaps"), @"Show converted beatmaps"); + + /// + /// "Display beatmaps from" + /// + public static LocalisableString StarsMinimum => new TranslatableString(getKey(@"stars_minimum"), @"Display beatmaps from"); + + /// + /// "up to" + /// + public static LocalisableString StarsMaximum => new TranslatableString(getKey(@"stars_maximum"), @"up to"); + + /// + /// "Random selection algorithm" + /// + public static LocalisableString RandomSelectionAlgorithm => new TranslatableString(getKey(@"random_selection_algorithm"), @"Random selection algorithm"); + + /// + /// "no limit" + /// + public static LocalisableString NoLimit => new TranslatableString(getKey(@"no_limit"), @"no limit"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index f7a3f4602f..af14cdc7b3 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -305,9 +305,11 @@ namespace osu.Game.Online.API { req.Perform(this); + if (req.CompletionState != APIRequestCompletionState.Completed) + return false; + // we could still be in initialisation, at which point we don't want to say we're Online yet. if (IsLoggedIn) state.Value = APIState.Online; - failureCount = 0; return true; } @@ -381,7 +383,7 @@ namespace osu.Game.Online.API } } - public bool IsLoggedIn => localUser.Value.Id > 1; + public bool IsLoggedIn => localUser.Value.Id > 1; // TODO: should this also be true if attempting to connect? public void Queue(APIRequest request) { diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index e117293ce6..cf17ed4b5d 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -84,7 +84,7 @@ namespace osu.Game.Online.API /// The state of this request, from an outside perspective. /// This is used to ensure correct notification events are fired. /// - private APIRequestCompletionState completionState; + public APIRequestCompletionState CompletionState { get; private set; } public void Perform(IAPIProvider api) { @@ -127,10 +127,10 @@ namespace osu.Game.Online.API { lock (completionStateLock) { - if (completionState != APIRequestCompletionState.Waiting) + if (CompletionState != APIRequestCompletionState.Waiting) return; - completionState = APIRequestCompletionState.Completed; + CompletionState = APIRequestCompletionState.Completed; } if (API == null) @@ -143,10 +143,10 @@ namespace osu.Game.Online.API { lock (completionStateLock) { - if (completionState != APIRequestCompletionState.Waiting) + if (CompletionState != APIRequestCompletionState.Waiting) return; - completionState = APIRequestCompletionState.Failed; + CompletionState = APIRequestCompletionState.Failed; } if (API == null) @@ -161,7 +161,7 @@ namespace osu.Game.Online.API { lock (completionStateLock) { - if (completionState != APIRequestCompletionState.Waiting) + if (CompletionState != APIRequestCompletionState.Waiting) return; WebRequest?.Abort(); @@ -200,7 +200,7 @@ namespace osu.Game.Online.API get { lock (completionStateLock) - return completionState == APIRequestCompletionState.Failed; + return CompletionState == APIRequestCompletionState.Failed; } } diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index ae9199c428..0e4ea694aa 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -43,7 +43,8 @@ namespace osu.Game.Online.Chat RegexOptions.IgnoreCase); // 00:00:000 (1,2,3) - test - private static readonly Regex time_regex = new Regex(@"\d\d:\d\d:\d\d\d? [^-]*"); + // regex from https://github.com/ppy/osu-web/blob/651a9bac2b60d031edd7e33b8073a469bf11edaa/resources/assets/coffee/_classes/beatmap-discussion-helper.coffee#L10 + private static readonly Regex time_regex = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\((?:\d+[,|])*\d+\))?)"); // #osu private static readonly Regex channel_regex = new Regex(@"(#[a-zA-Z]+[a-zA-Z0-9]+)"); diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 8b0caddbc6..6ed2055e65 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Chat; @@ -22,7 +23,7 @@ namespace osu.Game.Online.Chat { public readonly Bindable Channel = new Bindable(); - private readonly FocusedTextBox textbox; + protected readonly ChatTextBox Textbox; protected ChannelManager ChannelManager; @@ -30,6 +31,8 @@ namespace osu.Game.Online.Chat private readonly bool postingTextbox; + protected readonly Box Background; + private const float textbox_height = 30; /// @@ -44,7 +47,7 @@ namespace osu.Game.Online.Chat InternalChildren = new Drawable[] { - new Box + Background = new Box { Colour = Color4.Black, Alpha = 0.8f, @@ -54,7 +57,7 @@ namespace osu.Game.Online.Chat if (postingTextbox) { - AddInternal(textbox = new FocusedTextBox + AddInternal(Textbox = new ChatTextBox { RelativeSizeAxes = Axes.X, Height = textbox_height, @@ -65,7 +68,7 @@ namespace osu.Game.Online.Chat Origin = Anchor.BottomLeft, }); - textbox.OnCommit += postMessage; + Textbox.OnCommit += postMessage; } Channel.BindValueChanged(channelChanged); @@ -82,7 +85,7 @@ namespace osu.Game.Online.Chat private void postMessage(TextBox sender, bool newtext) { - var text = textbox.Text.Trim(); + var text = Textbox.Text.Trim(); if (string.IsNullOrWhiteSpace(text)) return; @@ -92,7 +95,7 @@ namespace osu.Game.Online.Chat else ChannelManager?.PostMessage(text, target: Channel.Value); - textbox.Text = string.Empty; + Textbox.Text = string.Empty; } protected virtual ChatLine CreateMessage(Message message) => new StandAloneMessage(message); @@ -110,6 +113,25 @@ namespace osu.Game.Online.Chat AddInternal(drawableChannel); } + public class ChatTextBox : FocusedTextBox + { + protected override void LoadComplete() + { + base.LoadComplete(); + + BackgroundUnfocused = new Color4(10, 10, 10, 10); + BackgroundFocused = new Color4(10, 10, 10, 255); + } + + protected override void OnFocusLost(FocusLostEvent e) + { + base.OnFocusLost(e); + FocusLost?.Invoke(); + } + + public Action FocusLost; + } + public class StandAloneDrawableChannel : DrawableChannel { public Func CreateChatLineAction; diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index d2dba8a402..e9d6960c71 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -148,7 +148,12 @@ namespace osu.Game.Online }); if (RuntimeInfo.SupportsJIT && preferMessagePack) - builder.AddMessagePackProtocol(); + { + builder.AddMessagePackProtocol(options => + { + options.SerializerOptions = SignalRUnionWorkaroundResolver.OPTIONS; + }); + } else { // eventually we will precompile resolvers for messagepack, but this isn't working currently diff --git a/osu.Game/Online/Multiplayer/MatchRoomState.cs b/osu.Game/Online/Multiplayer/MatchRoomState.cs index 5b662af100..edd34fb5a3 100644 --- a/osu.Game/Online/Multiplayer/MatchRoomState.cs +++ b/osu.Game/Online/Multiplayer/MatchRoomState.cs @@ -15,9 +15,9 @@ namespace osu.Game.Online.Multiplayer /// [Serializable] [MessagePackObject] - [Union(0, typeof(TeamVersusRoomState))] - // TODO: this will need to be abstract or interface when/if we get messagepack working. for now it isn't as it breaks json serialisation. - public class MatchRoomState + [Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + // TODO: abstract breaks json serialisation. attention will be required for iOS support (unless we get messagepack AOT working instead). + public abstract class MatchRoomState { } } diff --git a/osu.Game/Online/Multiplayer/MatchUserRequest.cs b/osu.Game/Online/Multiplayer/MatchUserRequest.cs index 15c3ad0776..8c6809e7f3 100644 --- a/osu.Game/Online/Multiplayer/MatchUserRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchUserRequest.cs @@ -3,6 +3,7 @@ using System; using MessagePack; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; namespace osu.Game.Online.Multiplayer { @@ -11,6 +12,7 @@ namespace osu.Game.Online.Multiplayer /// [Serializable] [MessagePackObject] + [Union(0, typeof(ChangeTeamRequest))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. public abstract class MatchUserRequest { } diff --git a/osu.Game/Online/Multiplayer/MatchUserState.cs b/osu.Game/Online/Multiplayer/MatchUserState.cs index f457191bb5..69245deba0 100644 --- a/osu.Game/Online/Multiplayer/MatchUserState.cs +++ b/osu.Game/Online/Multiplayer/MatchUserState.cs @@ -15,9 +15,9 @@ namespace osu.Game.Online.Multiplayer /// [Serializable] [MessagePackObject] - [Union(0, typeof(TeamVersusUserState))] - // TODO: this will need to be abstract or interface when/if we get messagepack working. for now it isn't as it breaks json serialisation. - public class MatchUserState + [Union(0, typeof(TeamVersusUserState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + // TODO: abstract breaks json serialisation. attention will be required for iOS support (unless we get messagepack AOT working instead). + public abstract class MatchUserState { } } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index c38a648a6a..965674c2f2 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -39,7 +39,7 @@ namespace osu.Game.Online.Multiplayer { // Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization. // More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code. - connector = api.GetHubConnector(nameof(OnlineMultiplayerClient), endpoint, false); + connector = api.GetHubConnector(nameof(OnlineMultiplayerClient), endpoint); if (connector != null) { diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index 806c0047e7..243be8da44 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -47,39 +47,13 @@ namespace osu.Game.Online pollIfNecessary(); } - private bool pollIfNecessary() + /// + /// Immediately performs a . + /// + public void PollImmediately() { - // we must be loaded so we have access to clock. - if (!IsLoaded) return false; - - // there's already a poll process running. - if (pollingActive) return false; - - // don't try polling if the time between polls hasn't been set. - if (TimeBetweenPolls.Value == 0) return false; - - if (!lastTimePolled.HasValue) - { - doPoll(); - return true; - } - - if (Time.Current - lastTimePolled.Value > TimeBetweenPolls.Value) - { - doPoll(); - return true; - } - - // not enough time has passed since the last poll. we do want to schedule a poll to happen, though. + lastTimePolled = Time.Current - TimeBetweenPolls.Value; scheduleNextPoll(); - return false; - } - - private void doPoll() - { - scheduledPoll = null; - pollingActive = true; - Poll().ContinueWith(_ => pollComplete()); } /// @@ -90,13 +64,11 @@ namespace osu.Game.Online return Task.CompletedTask; } - /// - /// Immediately performs a . - /// - public void PollImmediately() + private void doPoll() { - lastTimePolled = Time.Current - TimeBetweenPolls.Value; - scheduleNextPoll(); + scheduledPoll = null; + pollingActive = true; + Poll().ContinueWith(_ => pollComplete()); } /// @@ -111,6 +83,33 @@ namespace osu.Game.Online pollIfNecessary(); } + private void pollIfNecessary() + { + // we must be loaded so we have access to clock. + if (!IsLoaded) return; + + // there's already a poll process running. + if (pollingActive) return; + + // don't try polling if the time between polls hasn't been set. + if (TimeBetweenPolls.Value == 0) return; + + if (!lastTimePolled.HasValue) + { + doPoll(); + return; + } + + if (Time.Current - lastTimePolled.Value > TimeBetweenPolls.Value) + { + doPoll(); + return; + } + + // not enough time has passed since the last poll. we do want to schedule a poll to happen, though. + scheduleNextPoll(); + } + private void scheduleNextPoll() { scheduledPoll?.Cancel(); diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 4bd5b1a788..d964060f10 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -134,7 +134,7 @@ namespace osu.Game.Online.Rooms /// The position of this in the list. This is not read from or written to the API. /// [JsonIgnore] - public readonly Bindable Position = new Bindable(-1); + public readonly Bindable Position = new Bindable(-1); // Todo: This does not need to exist. public Room() { diff --git a/osu.Game/Online/Rooms/RoomCategory.cs b/osu.Game/Online/Rooms/RoomCategory.cs index bb9f1298d3..a1dcfa5fd9 100644 --- a/osu.Game/Online/Rooms/RoomCategory.cs +++ b/osu.Game/Online/Rooms/RoomCategory.cs @@ -8,6 +8,5 @@ namespace osu.Game.Online.Rooms // used for osu-web deserialization so names shouldn't be changed. Normal, Spotlight, - Realtime, } } diff --git a/osu.Game/Online/SignalRUnionWorkaroundResolver.cs b/osu.Game/Online/SignalRUnionWorkaroundResolver.cs new file mode 100644 index 0000000000..e44da044cc --- /dev/null +++ b/osu.Game/Online/SignalRUnionWorkaroundResolver.cs @@ -0,0 +1,61 @@ +// 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 MessagePack; +using MessagePack.Formatters; +using MessagePack.Resolvers; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; + +namespace osu.Game.Online +{ + /// + /// Handles SignalR being unable to comprehend [Union] types correctly by redirecting to a known base (union) type. + /// See https://github.com/dotnet/aspnetcore/issues/7298. + /// + public class SignalRUnionWorkaroundResolver : IFormatterResolver + { + public static readonly MessagePackSerializerOptions OPTIONS = + MessagePackSerializerOptions.Standard.WithResolver(new SignalRUnionWorkaroundResolver()); + + private static readonly Dictionary formatter_map = new Dictionary + { + { typeof(TeamVersusUserState), new TypeRedirectingFormatter() }, + { typeof(TeamVersusRoomState), new TypeRedirectingFormatter() }, + { typeof(ChangeTeamRequest), new TypeRedirectingFormatter() }, + + // These should not be required. The fallback should work. But something is weird with the way caching is done. + // For future adventurers, I would not advise looking into this further. It's likely not worth the effort. + { typeof(MatchUserState), new TypeRedirectingFormatter() }, + { typeof(MatchRoomState), new TypeRedirectingFormatter() }, + { typeof(MatchUserRequest), new TypeRedirectingFormatter() }, + { typeof(MatchServerEvent), new TypeRedirectingFormatter() }, + }; + + public IMessagePackFormatter GetFormatter() + { + if (formatter_map.TryGetValue(typeof(T), out var formatter)) + return (IMessagePackFormatter)formatter; + + return StandardResolver.Instance.GetFormatter(); + } + + public class TypeRedirectingFormatter : IMessagePackFormatter + { + private readonly IMessagePackFormatter baseFormatter; + + public TypeRedirectingFormatter() + { + baseFormatter = StandardResolver.Instance.GetFormatter(); + } + + public void Serialize(ref MessagePackWriter writer, TActual value, MessagePackSerializerOptions options) => + baseFormatter.Serialize(ref writer, (TBase)(object)value, StandardResolver.Options); + + public TActual Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => + (TActual)(object)baseFormatter.Deserialize(ref reader, StandardResolver.Options); + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fb682e0909..4d952c39c6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -62,7 +62,7 @@ namespace osu.Game /// The full osu! experience. Builds on top of to add menus and binding logic /// for initial components that are generally retrieved via DI. /// - public class OsuGame : OsuGameBase, IKeyBindingHandler + public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo { /// /// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications). @@ -1018,10 +1018,13 @@ namespace osu.Game var horizontalOffset = 0f; + // Content.ToLocalSpace() is used instead of this.ToLocalSpace() to correctly calculate the offset with scaling modes active. + // Content is a child of a scaling container with ScalingMode.Everything set, while the game itself is never scaled. + // this avoids a visible jump in the positioning of the screen offset container. if (Settings.IsLoaded && Settings.IsPresent) - horizontalOffset += ToLocalSpace(Settings.ScreenSpaceDrawQuad.TopRight).X * SIDE_OVERLAY_OFFSET_RATIO; + horizontalOffset += Content.ToLocalSpace(Settings.ScreenSpaceDrawQuad.TopRight).X * SIDE_OVERLAY_OFFSET_RATIO; if (Notifications.IsLoaded && Notifications.IsPresent) - horizontalOffset += (ToLocalSpace(Notifications.ScreenSpaceDrawQuad.TopLeft).X - DrawWidth) * SIDE_OVERLAY_OFFSET_RATIO; + horizontalOffset += (Content.ToLocalSpace(Notifications.ScreenSpaceDrawQuad.TopLeft).X - Content.DrawWidth) * SIDE_OVERLAY_OFFSET_RATIO; ScreenOffsetContainer.X = horizontalOffset; @@ -1085,5 +1088,7 @@ namespace osu.Game if (newScreen == null) Exit(); } + + IBindable ILocalUserPlayInfo.IsPlaying => LocalUserPlaying; } } diff --git a/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs index 4bbc3569fe..3aa9aa5ca5 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs @@ -139,19 +139,24 @@ namespace osu.Game.Overlays.BeatmapListing.Panels LoadComponentAsync(Preview = previewTrackManager.Get(beatmapSet), preview => { - // beatmapset may have changed. - if (Preview != preview) - return; + // Make sure that we schedule to after the next audio frame to fix crashes in single-threaded execution. + // See: https://github.com/ppy/osu-framework/issues/4692 + Schedule(() => + { + // beatmapset may have changed. + if (Preview != preview) + return; - AddInternal(preview); - loading = false; - // make sure that the update of value of Playing (and the ensuing value change callbacks) - // are marshaled back to the update thread. - preview.Stopped += () => Schedule(() => playing.Value = false); + AddInternal(preview); + loading = false; + // make sure that the update of value of Playing (and the ensuing value change callbacks) + // are marshaled back to the update thread. + preview.Stopped += () => Schedule(() => playing.Value = false); - // user may have changed their mind. - if (playing.Value) - attemptStart(); + // user may have changed their mind. + if (playing.Value) + attemptStart(); + }); }); } else diff --git a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs index f617b4fc82..508c8399b6 100644 --- a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs +++ b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs @@ -24,6 +24,7 @@ namespace osu.Game.Overlays.Changelog public class ChangelogSupporterPromo : CompositeDrawable { private const float image_container_width = 164; + private const float heart_size = 75; private readonly FillFlowContainer textContainer; private readonly Container imageContainer; @@ -134,18 +135,30 @@ namespace osu.Game.Overlays.Changelog { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Margin = new MarginPadding { Bottom = 28 }, RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fill, Texture = textures.Get(@"Online/supporter-pippi"), }, - new Sprite + new Container { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Width = 75, - Height = 75, + Size = new Vector2(heart_size), Margin = new MarginPadding { Top = 70 }, - Texture = textures.Get(@"Online/supporter-heart"), + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = colour.Pink, + Radius = 10, + Roundness = heart_size / 2, + }, + Child = new Sprite + { + Size = new Vector2(heart_size), + Texture = textures.Get(@"Online/supporter-heart"), + }, }, }; } diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 01cfe9a55b..6f4f568eb2 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -110,15 +110,15 @@ namespace osu.Game.Overlays.Chat EdgeEffect = new EdgeEffectParameters { Roundness = 1, - Offset = new Vector2(0, 3), - Radius = 3, + Radius = 1, Colour = Color4.Black.Opacity(0.3f), + Offset = new Vector2(0, 1), Type = EdgeEffectType.Shadow, }, Child = new Container { AutoSizeAxes = Axes.Both, - Y = 3, + Y = 0, Masking = true, CornerRadius = 4, Children = new Drawable[] diff --git a/osu.Game/Overlays/RestoreDefaultValueButton.cs b/osu.Game/Overlays/RestoreDefaultValueButton.cs index fd3ee16fe6..87a294cc10 100644 --- a/osu.Game/Overlays/RestoreDefaultValueButton.cs +++ b/osu.Game/Overlays/RestoreDefaultValueButton.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; @@ -38,12 +39,12 @@ namespace osu.Game.Overlays current.ValueChanged += _ => UpdateState(); current.DefaultChanged += _ => UpdateState(); current.DisabledChanged += _ => UpdateState(); - UpdateState(); + + if (IsLoaded) + UpdateState(); } } - private Color4 buttonColour; - private bool hovering; public RestoreDefaultValueButton() @@ -58,12 +59,11 @@ namespace osu.Game.Overlays private void load(OsuColour colour) { BackgroundColour = colour.Yellow; - buttonColour = colour.Yellow; Content.Width = 0.33f; Content.CornerRadius = 3; Content.EdgeEffect = new EdgeEffectParameters { - Colour = buttonColour.Opacity(0.1f), + Colour = BackgroundColour.Opacity(0.1f), Type = EdgeEffectType.Glow, Radius = 2, }; @@ -81,7 +81,10 @@ namespace osu.Game.Overlays protected override void LoadComplete() { base.LoadComplete(); - UpdateState(); + + // avoid unnecessary transforms on first display. + Alpha = currentAlpha; + Background.Colour = currentColour; } public LocalisableString TooltipText => "revert to default"; @@ -101,14 +104,16 @@ namespace osu.Game.Overlays public void UpdateState() => Scheduler.AddOnce(updateState); + private float currentAlpha => current.IsDefault ? 0f : hovering && !current.Disabled ? 1f : 0.65f; + private ColourInfo currentColour => current.Disabled ? Color4.Gray : BackgroundColour; + private void updateState() { if (current == null) return; - this.FadeTo(current.IsDefault ? 0f : - hovering && !current.Disabled ? 1f : 0.65f, 200, Easing.OutQuint); - this.FadeColour(current.Disabled ? Color4.Gray : buttonColour, 200, Easing.OutQuint); + this.FadeTo(currentAlpha, 200, Easing.OutQuint); + Background.FadeColour(currentColour, 200, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index d64f176468..d697b45424 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -8,29 +8,39 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Audio { public class AudioDevicesSettings : SettingsSubsection { - protected override LocalisableString Header => "Devices"; + protected override LocalisableString Header => AudioSettingsStrings.AudioDevicesHeader; [Resolved] private AudioManager audio { get; set; } private SettingsDropdown dropdown; - protected override void Dispose(bool isDisposing) + [BackgroundDependencyLoader] + private void load() { - base.Dispose(isDisposing); - - if (audio != null) + Children = new Drawable[] { - audio.OnNewDevice -= onDeviceChanged; - audio.OnLostDevice -= onDeviceChanged; - } + dropdown = new AudioDeviceSettingsDropdown + { + Keywords = new[] { "speaker", "headphone", "output" } + } + }; + + updateItems(); + + audio.OnNewDevice += onDeviceChanged; + audio.OnLostDevice += onDeviceChanged; + dropdown.Current = audio.AudioDevice; } + private void onDeviceChanged(string name) => updateItems(); + private void updateItems() { var deviceItems = new List { string.Empty }; @@ -49,26 +59,15 @@ namespace osu.Game.Overlays.Settings.Sections.Audio dropdown.Items = deviceItems.Distinct().ToList(); } - private void onDeviceChanged(string name) => updateItems(); - - protected override void LoadComplete() + protected override void Dispose(bool isDisposing) { - base.LoadComplete(); + base.Dispose(isDisposing); - Children = new Drawable[] + if (audio != null) { - dropdown = new AudioDeviceSettingsDropdown - { - Keywords = new[] { "speaker", "headphone", "output" } - } - }; - - updateItems(); - - dropdown.Current = audio.AudioDevice; - - audio.OnNewDevice += onDeviceChanged; - audio.OnLostDevice += onDeviceChanged; + audio.OnNewDevice -= onDeviceChanged; + audio.OnLostDevice -= onDeviceChanged; + } } private class AudioDeviceSettingsDropdown : SettingsDropdown @@ -78,7 +77,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio private class AudioDeviceDropdownControl : DropdownControl { protected override LocalisableString GenerateItemText(string item) - => string.IsNullOrEmpty(item) ? "Default" : base.GenerateItemText(item); + => string.IsNullOrEmpty(item) ? CommonStrings.Default : base.GenerateItemText(item); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index 7f2e377c83..9345d3fcc7 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -1,17 +1,22 @@ // 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.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Audio { public class OffsetSettings : SettingsSubsection { - protected override LocalisableString Header => "Offset Adjustment"; + protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader; + + public override IEnumerable FilterTerms => base.FilterTerms.Concat(new[] { "universal", "uo", "timing" }); [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -20,13 +25,13 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { new SettingsSlider { - LabelText = "Audio offset", + LabelText = AudioSettingsStrings.AudioOffset, Current = config.GetBindable(OsuSetting.AudioOffset), KeyboardStep = 1f }, new SettingsButton { - Text = "Offset wizard" + Text = AudioSettingsStrings.OffsetWizard } }; } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs index 8f88b03471..00c1cb8f43 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs @@ -6,12 +6,13 @@ using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Audio { public class VolumeSettings : SettingsSubsection { - protected override LocalisableString Header => "Volume"; + protected override LocalisableString Header => AudioSettingsStrings.VolumeHeader; [BackgroundDependencyLoader] private void load(AudioManager audio, OsuConfigManager config) @@ -20,28 +21,28 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { new SettingsSlider { - LabelText = "Master", + LabelText = AudioSettingsStrings.MasterVolume, Current = audio.Volume, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { - LabelText = "Master (window inactive)", + LabelText = AudioSettingsStrings.MasterVolumeInactive, Current = config.GetBindable(OsuSetting.VolumeInactive), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { - LabelText = "Effect", + LabelText = AudioSettingsStrings.EffectVolume, Current = audio.VolumeSample, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { - LabelText = "Music", + LabelText = AudioSettingsStrings.MusicVolume, Current = audio.VolumeTrack, KeyboardStep = 0.01f, DisplayAsPercentage = true diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs index 7072d8e63d..694da0529a 100644 --- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs +++ b/osu.Game/Overlays/Settings/Sections/AudioSection.cs @@ -3,15 +3,17 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Localisation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Audio; namespace osu.Game.Overlays.Settings.Sections { public class AudioSection : SettingsSection { - public override string Header => "Audio"; + public override LocalisableString Header => AudioSettingsStrings.AudioSectionHeader; public override Drawable CreateIcon() => new SpriteIcon { diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs index 2b868cab85..25e20911b8 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs @@ -6,13 +6,14 @@ using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Framework.Screens; +using osu.Game.Localisation; using osu.Game.Screens.Import; namespace osu.Game.Overlays.Settings.Sections.Debug { public class GeneralSettings : SettingsSubsection { - protected override LocalisableString Header => "General"; + protected override LocalisableString Header => DebugSettingsStrings.GeneralHeader; [BackgroundDependencyLoader(true)] private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, OsuGame game) @@ -21,18 +22,18 @@ namespace osu.Game.Overlays.Settings.Sections.Debug { new SettingsCheckbox { - LabelText = "Show log overlay", + LabelText = DebugSettingsStrings.ShowLogOverlay, Current = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) }, new SettingsCheckbox { - LabelText = "Bypass front-to-back render pass", + LabelText = DebugSettingsStrings.BypassFrontToBackPass, Current = config.GetBindable(DebugSetting.BypassFrontToBackPass) } }; Add(new SettingsButton { - Text = "Import files", + Text = DebugSettingsStrings.ImportFiles, Action = () => game?.PerformFromScreen(menu => menu.Push(new FileImportScreen())) }); } diff --git a/osu.Game/Overlays/Settings/Sections/Debug/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/MemorySettings.cs index bf7fb351c0..07fb0aca5a 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/MemorySettings.cs @@ -6,12 +6,13 @@ using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Framework.Platform; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Debug { public class MemorySettings : SettingsSubsection { - protected override LocalisableString Header => "Memory"; + protected override LocalisableString Header => DebugSettingsStrings.MemoryHeader; [BackgroundDependencyLoader] private void load(FrameworkDebugConfigManager config, GameHost host) @@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections.Debug { new SettingsButton { - Text = "Clear all caches", + Text = DebugSettingsStrings.ClearAllCaches, Action = host.Collect }, }; diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index 44d4088972..aa85ec920c 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs @@ -3,13 +3,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Debug; namespace osu.Game.Overlays.Settings.Sections { public class DebugSection : SettingsSection { - public override string Header => "Debug"; + public override LocalisableString Header => DebugSettingsStrings.DebugSectionHeader; public override Drawable CreateIcon() => new SpriteIcon { diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 69aa57082a..3a0265e453 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -6,13 +6,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation; using osu.Game.Rulesets.Scoring; namespace osu.Game.Overlays.Settings.Sections.Gameplay { public class GeneralSettings : SettingsSubsection { - protected override LocalisableString Header => "General"; + protected override LocalisableString Header => GameplaySettingsStrings.GeneralHeader; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -21,62 +22,62 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { new SettingsSlider { - LabelText = "Background dim", + LabelText = GameplaySettingsStrings.BackgroundDim, Current = config.GetBindable(OsuSetting.DimLevel), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { - LabelText = "Background blur", + LabelText = GameplaySettingsStrings.BackgroundBlur, Current = config.GetBindable(OsuSetting.BlurLevel), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsCheckbox { - LabelText = "Lighten playfield during breaks", + LabelText = GameplaySettingsStrings.LightenDuringBreaks, Current = config.GetBindable(OsuSetting.LightenDuringBreaks) }, new SettingsEnumDropdown { - LabelText = "HUD overlay visibility mode", + LabelText = GameplaySettingsStrings.HUDVisibilityMode, Current = config.GetBindable(OsuSetting.HUDVisibilityMode) }, new SettingsCheckbox { - LabelText = "Show difficulty graph on progress bar", - Current = config.GetBindable(OsuSetting.ShowDifficultyGraph) + LabelText = GameplaySettingsStrings.ShowDifficultyGraph, + Current = config.GetBindable(OsuSetting.ShowProgressGraph) }, new SettingsCheckbox { - LabelText = "Show health display even when you can't fail", + LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), Keywords = new[] { "hp", "bar" } }, new SettingsCheckbox { - LabelText = "Fade playfield to red when health is low", + LabelText = GameplaySettingsStrings.FadePlayfieldWhenHealthLow, Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), }, new SettingsCheckbox { - LabelText = "Always show key overlay", + LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay, Current = config.GetBindable(OsuSetting.KeyOverlay) }, new SettingsCheckbox { - LabelText = "Positional hitsounds", + LabelText = GameplaySettingsStrings.PositionalHitsounds, Current = config.GetBindable(OsuSetting.PositionalHitSounds) }, new SettingsCheckbox { - LabelText = "Always play first combo break sound", + LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak, Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak) }, new SettingsEnumDropdown { - LabelText = "Score display mode", + LabelText = GameplaySettingsStrings.ScoreDisplayMode, Current = config.GetBindable(OsuSetting.ScoreDisplayMode), Keywords = new[] { "scoring" } }, @@ -86,7 +87,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { Add(new SettingsCheckbox { - LabelText = "Disable Windows key during gameplay", + LabelText = GameplaySettingsStrings.DisableWinKey, Current = config.GetBindable(OsuSetting.GameplayDisableWinKey) }); } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs index ec9ddde2da..dfa060e8d5 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs @@ -6,12 +6,13 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Gameplay { public class ModsSettings : SettingsSubsection { - protected override LocalisableString Header => "Mods"; + protected override LocalisableString Header => GameplaySettingsStrings.ModsHeader; public override IEnumerable FilterTerms => base.FilterTerms.Concat(new[] { "mod" }); @@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { new SettingsCheckbox { - LabelText = "Increase visibility of first object when visual impairment mods are enabled", + LabelText = GameplaySettingsStrings.IncreaseFirstObjectVisibility, Current = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility), }, }; diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index acb94a6a01..42d9d48d73 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -9,12 +9,14 @@ using osu.Game.Rulesets; using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Framework.Logging; +using osu.Framework.Localisation; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections { public class GameplaySection : SettingsSection { - public override string Header => "Gameplay"; + public override LocalisableString Header => GameplaySettingsStrings.GameplaySectionHeader; public override Drawable CreateIcon() => new SpriteIcon { diff --git a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs index c6c752e2fd..200618c469 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.General private SettingsDropdown languageSelection; private Bindable frameworkLocale; - protected override LocalisableString Header => "Language"; + protected override LocalisableString Header => GeneralSettingsStrings.LanguageHeader; [BackgroundDependencyLoader] private void load(FrameworkConfigManager frameworkConfig) @@ -27,11 +27,11 @@ namespace osu.Game.Overlays.Settings.Sections.General { languageSelection = new SettingsEnumDropdown { - LabelText = "Language", + LabelText = GeneralSettingsStrings.LanguageDropdown, }, new SettingsCheckbox { - LabelText = "Prefer metadata in original language", + LabelText = GeneralSettingsStrings.PreferOriginalMetadataLanguage, Current = frameworkConfig.GetBindable(FrameworkSetting.ShowUnicode) }, }; diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index dd20e1d7ef..aa37748653 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -9,6 +9,7 @@ using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Configuration; +using osu.Game.Localisation; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Settings.Sections.Maintenance; using osu.Game.Updater; @@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections.General [Resolved(CanBeNull = true)] private UpdateManager updateManager { get; set; } - protected override LocalisableString Header => "Updates"; + protected override LocalisableString Header => GeneralSettingsStrings.UpdateHeader; private SettingsButton checkForUpdatesButton; @@ -32,7 +33,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { Add(new SettingsEnumDropdown { - LabelText = "Release stream", + LabelText = GeneralSettingsStrings.ReleaseStream, Current = config.GetBindable(OsuSetting.ReleaseStream), }); @@ -40,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { Add(checkForUpdatesButton = new SettingsButton { - Text = "Check for updates", + Text = GeneralSettingsStrings.CheckUpdate, Action = () => { checkForUpdatesButton.Enabled.Value = false; @@ -65,13 +66,13 @@ namespace osu.Game.Overlays.Settings.Sections.General { Add(new SettingsButton { - Text = "Open osu! folder", + Text = GeneralSettingsStrings.OpenOsuFolder, Action = storage.OpenInNativeExplorer, }); Add(new SettingsButton { - Text = "Change folder location...", + Text = GeneralSettingsStrings.ChangeFolderLocation, Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) }); } diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index fefc3fe6a7..87e9f34833 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -3,13 +3,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.General; namespace osu.Game.Overlays.Settings.Sections { public class GeneralSection : SettingsSection { - public override string Header => "General"; + public override LocalisableString Header => GeneralSettingsStrings.GeneralSectionHeader; public override Drawable CreateIcon() => new SpriteIcon { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index f889cfca0f..20b1d8d801 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -5,12 +5,13 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Graphics { public class DetailSettings : SettingsSubsection { - protected override LocalisableString Header => "Detail Settings"; + protected override LocalisableString Header => GraphicsSettingsStrings.DetailSettingsHeader; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -19,22 +20,22 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { new SettingsCheckbox { - LabelText = "Storyboard / Video", + LabelText = GraphicsSettingsStrings.StoryboardVideo, Current = config.GetBindable(OsuSetting.ShowStoryboard) }, new SettingsCheckbox { - LabelText = "Hit Lighting", + LabelText = GraphicsSettingsStrings.HitLighting, Current = config.GetBindable(OsuSetting.HitLighting) }, new SettingsEnumDropdown { - LabelText = "Screenshot format", + LabelText = GraphicsSettingsStrings.ScreenshotFormat, Current = config.GetBindable(OsuSetting.ScreenshotFormat) }, new SettingsCheckbox { - LabelText = "Show menu cursor in screenshots", + LabelText = GraphicsSettingsStrings.ShowCursorInScreenshots, Current = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor) } }; diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 91208cb78a..adf1453d1a 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -16,13 +16,14 @@ using osu.Framework.Platform; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osuTK.Graphics; namespace osu.Game.Overlays.Settings.Sections.Graphics { public class LayoutSettings : SettingsSubsection { - protected override LocalisableString Header => "Layout"; + protected override LocalisableString Header => GraphicsSettingsStrings.LayoutHeader; private FillFlowContainer> scalingSettings; @@ -67,20 +68,20 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { windowModeDropdown = new SettingsDropdown { - LabelText = "Screen mode", + LabelText = GraphicsSettingsStrings.ScreenMode, ItemSource = windowModes, Current = config.GetBindable(FrameworkSetting.WindowMode), }, resolutionDropdown = new ResolutionSettingsDropdown { - LabelText = "Resolution", + LabelText = GraphicsSettingsStrings.Resolution, ShowsDefaultIndicator = false, ItemSource = resolutions, Current = sizeFullscreen }, new SettingsSlider { - LabelText = "UI Scaling", + LabelText = GraphicsSettingsStrings.UIScaling, TransferValueOnCommit = true, Current = osuConfig.GetBindable(OsuSetting.UIScale), KeyboardStep = 0.01f, @@ -88,7 +89,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }, new SettingsEnumDropdown { - LabelText = "Screen Scaling", + LabelText = GraphicsSettingsStrings.ScreenScaling, Current = osuConfig.GetBindable(OsuSetting.Scaling), Keywords = new[] { "scale", "letterbox" }, }, @@ -97,35 +98,33 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - AutoSizeDuration = transition_duration, - AutoSizeEasing = Easing.OutQuint, Masking = true, Children = new[] { new SettingsSlider { - LabelText = "Horizontal position", + LabelText = GraphicsSettingsStrings.HorizontalPosition, Current = scalingPositionX, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { - LabelText = "Vertical position", + LabelText = GraphicsSettingsStrings.VerticalPosition, Current = scalingPositionY, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { - LabelText = "Horizontal scale", + LabelText = GraphicsSettingsStrings.HorizontalScale, Current = scalingSizeX, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { - LabelText = "Vertical scale", + LabelText = GraphicsSettingsStrings.VerticalScale, Current = scalingSizeY, KeyboardStep = 0.01f, DisplayAsPercentage = true @@ -145,9 +144,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { updateResolutionDropdown(); - const string not_fullscreen_note = "Running without fullscreen mode will increase your input latency!"; - - windowModeDropdown.WarningText = mode.NewValue != WindowMode.Fullscreen ? not_fullscreen_note : string.Empty; + windowModeDropdown.WarningText = mode.NewValue != WindowMode.Fullscreen ? GraphicsSettingsStrings.NotFullscreenNote : default; }, true); windowModes.BindCollectionChanged((sender, args) => @@ -177,13 +174,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingMode.BindValueChanged(mode => { scalingSettings.ClearTransforms(); - scalingSettings.AutoSizeAxes = mode.NewValue != ScalingMode.Off ? Axes.Y : Axes.None; + scalingSettings.AutoSizeDuration = transition_duration; + scalingSettings.AutoSizeEasing = Easing.OutQuint; - if (mode.NewValue == ScalingMode.Off) - scalingSettings.ResizeHeightTo(0, transition_duration, Easing.OutQuint); + updateScalingModeVisibility(); + }); - scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything); - }, true); + // initial update bypasses transforms + updateScalingModeVisibility(); void updateResolutionDropdown() { @@ -192,6 +190,15 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics else resolutionDropdown.Hide(); } + + void updateScalingModeVisibility() + { + if (scalingMode.Value == ScalingMode.Off) + scalingSettings.ResizeHeightTo(0, transition_duration, Easing.OutQuint); + + scalingSettings.AutoSizeAxes = scalingMode.Value != ScalingMode.Off ? Axes.Y : Axes.None; + scalingSettings.ForEach(s => s.TransferValueOnCommit = scalingMode.Value == ScalingMode.Everything); + } } private void bindPreviewEvent(Bindable bindable) @@ -245,7 +252,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics protected override LocalisableString GenerateItemText(Size item) { if (item == new Size(9999, 9999)) - return "Default"; + return CommonStrings.Default; return $"{item.Width}x{item.Height}"; } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index 2210c7911e..653f30a018 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -7,12 +7,13 @@ using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Game.Configuration; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Graphics { public class RendererSettings : SettingsSubsection { - protected override LocalisableString Header => "Renderer"; + protected override LocalisableString Header => GraphicsSettingsStrings.RendererHeader; private SettingsEnumDropdown frameLimiterDropdown; @@ -25,17 +26,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics // TODO: this needs to be a custom dropdown at some point frameLimiterDropdown = new SettingsEnumDropdown { - LabelText = "Frame limiter", + LabelText = GraphicsSettingsStrings.FrameLimiter, Current = config.GetBindable(FrameworkSetting.FrameSync) }, new SettingsEnumDropdown { - LabelText = "Threading mode", + LabelText = GraphicsSettingsStrings.ThreadingMode, Current = config.GetBindable(FrameworkSetting.ExecutionMode) }, new SettingsCheckbox { - LabelText = "Show FPS", + LabelText = GraphicsSettingsStrings.ShowFPS, Current = osuConfig.GetBindable(OsuSetting.ShowFpsDisplay) }, }; @@ -47,9 +48,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics frameLimiterDropdown.Current.BindValueChanged(limit => { - const string unlimited_frames_note = "Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. \"2x refresh rate\" is recommended."; - - frameLimiterDropdown.WarningText = limit.NewValue == FrameSync.Unlimited ? unlimited_frames_note : string.Empty; + frameLimiterDropdown.WarningText = limit.NewValue == FrameSync.Unlimited ? GraphicsSettingsStrings.UnlimitedFramesNote : default; }, true); } } diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index 4ade48031f..fd0718f9f2 100644 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs @@ -3,13 +3,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Graphics; namespace osu.Game.Overlays.Settings.Sections { public class GraphicsSection : SettingsSection { - public override string Header => "Graphics"; + public override LocalisableString Header => GraphicsSettingsStrings.GraphicsSectionHeader; public override Drawable CreateIcon() => new SpriteIcon { diff --git a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs index 9898a50320..3350ff4eaa 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Input.Bindings; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Input { @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Icon = FontAwesome.Solid.Globe }; - public override string Header => "Global"; + public override LocalisableString Header => InputSettingsStrings.GlobalKeyBindingHeader; public GlobalKeyBindingsSection(GlobalActionContainer manager) { @@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private class SongSelectKeyBindingSubsection : KeyBindingsSubsection { - protected override LocalisableString Header => "Song Select"; + protected override LocalisableString Header => InputSettingsStrings.SongSelectSection; public SongSelectKeyBindingSubsection(GlobalActionContainer manager) : base(null) @@ -50,7 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private class InGameKeyBindingsSubsection : KeyBindingsSubsection { - protected override LocalisableString Header => "In Game"; + protected override LocalisableString Header => InputSettingsStrings.InGameSection; public InGameKeyBindingsSubsection(GlobalActionContainer manager) : base(null) @@ -61,7 +62,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private class AudioControlKeyBindingsSubsection : KeyBindingsSubsection { - protected override LocalisableString Header => "Audio"; + protected override LocalisableString Header => InputSettingsStrings.AudioSection; public AudioControlKeyBindingsSubsection(GlobalActionContainer manager) : base(null) @@ -72,7 +73,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private class EditorKeyBindingsSubsection : KeyBindingsSubsection { - protected override LocalisableString Header => "Editor"; + protected override LocalisableString Header => InputSettingsStrings.EditorSection; public EditorKeyBindingsSubsection(GlobalActionContainer manager) : base(null) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs index 7cdc739b7c..67f1bb8d3e 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs @@ -4,13 +4,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Rulesets; namespace osu.Game.Overlays.Settings.Sections.Input { public class KeyBindingPanel : SettingsSubPanel { - protected override Drawable CreateHeader() => new SettingsHeader("key configuration", "Customise your keys!"); + protected override Drawable CreateHeader() => new SettingsHeader(InputSettingsStrings.KeyBindingPanelHeader, InputSettingsStrings.KeyBindingPanelDescription); [BackgroundDependencyLoader(permitNulls: true)] private void load(RulesetStore rulesets, GlobalActionContainer global) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 6e018597be..c38c516f21 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -385,7 +386,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { public CancelButton() { - Text = "Cancel"; + Text = CommonStrings.Cancel; Size = new Vector2(80, 20); } } @@ -394,7 +395,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { public ClearButton() { - Text = "Clear"; + Text = CommonStrings.Clear; Size = new Vector2(80, 20); } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index d65684fd37..ef5ccae1a0 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -10,6 +10,7 @@ using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets; +using osu.Game.Localisation; using osuTK; namespace osu.Game.Overlays.Settings.Sections.Input @@ -64,7 +65,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input [BackgroundDependencyLoader] private void load() { - Text = "Reset all bindings in section"; + Text = InputSettingsStrings.ResetSectionButton; RelativeSizeAxes = Axes.X; Width = 0.5f; Anchor = Anchor.TopCentre; diff --git a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs index 81a4d7eccd..5246051a4a 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Rulesets; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Icon = OsuIcon.Hot }; - public override string Header => ruleset.Name; + public override LocalisableString Header => ruleset.Name; private readonly RulesetInfo ruleset; diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index c561b693d8..b8b86d9069 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -22,6 +22,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { private readonly ITabletHandler tabletHandler; + private readonly Bindable enabled = new BindableBool(true); + private readonly Bindable areaOffset = new Bindable(); private readonly Bindable areaSize = new Bindable(); private readonly IBindable tablet = new Bindable(); @@ -74,7 +76,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = CommonStrings.Enabled, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Current = tabletHandler.Enabled + Current = enabled, }, noTabletMessage = new FillFlowContainer { @@ -194,6 +196,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input { base.LoadComplete(); + enabled.BindTo(tabletHandler.Enabled); + enabled.BindValueChanged(_ => Scheduler.AddOnce(updateVisibility)); + rotation.BindTo(tabletHandler.Rotation); areaOffset.BindTo(tabletHandler.AreaOffset); @@ -239,7 +244,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input tablet.BindTo(tabletHandler.Tablet); tablet.BindValueChanged(val => { - Scheduler.AddOnce(toggleVisibility); + Scheduler.AddOnce(updateVisibility); var tab = val.NewValue; @@ -259,19 +264,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, true); } - private void toggleVisibility() + private void updateVisibility() { - bool tabletFound = tablet.Value != null; - - if (!tabletFound) - { - mainSettings.Hide(); - noTabletMessage.Show(); - return; - } - - mainSettings.Show(); + mainSettings.Hide(); noTabletMessage.Hide(); + + if (!tabletHandler.Enabled.Value) + return; + + if (tablet.Value != null) + mainSettings.Show(); + else + noTabletMessage.Show(); } private void applyAspectRatio(BindableNumber sizeChanged) diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs index 366f39388a..d282ba5318 100644 --- a/osu.Game/Overlays/Settings/Sections/InputSection.cs +++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Handlers.Mouse; using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Localisation; using osu.Framework.Platform; +using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Input; namespace osu.Game.Overlays.Settings.Sections @@ -19,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections { private readonly KeyBindingPanel keyConfig; - public override string Header => "Input"; + public override LocalisableString Header => InputSettingsStrings.InputSectionHeader; [Resolved] private GameHost host { get; set; } @@ -95,7 +96,7 @@ namespace osu.Game.Overlays.Settings.Sections { new SettingsCheckbox { - LabelText = "Enabled", + LabelText = CommonStrings.Enabled, Current = handler.Enabled }, }; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs index 5392ba5d93..e509cac2f1 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs @@ -14,6 +14,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Graphics.Containers; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Maintenance { @@ -104,7 +105,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Origin = Anchor.Centre, Width = 300, Margin = new MarginPadding(10), - Text = "Select directory", + Text = MaintenanceSettingsStrings.SelectDirectory, Action = () => OnSelection(directorySelector.CurrentPath.Value) }, } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index b9a408b1f8..803c8332c1 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Scoring; using osu.Game.Skinning; @@ -37,7 +38,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { Add(importBeatmapsButton = new SettingsButton { - Text = "Import beatmaps from stable", + Text = MaintenanceSettingsStrings.ImportBeatmapsFromStable, Action = () => { importBeatmapsButton.Enabled.Value = false; @@ -48,7 +49,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Add(deleteBeatmapsButton = new DangerousSettingsButton { - Text = "Delete ALL beatmaps", + Text = MaintenanceSettingsStrings.DeleteAllBeatmaps, Action = () => { dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => @@ -63,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { Add(importScoresButton = new SettingsButton { - Text = "Import scores from stable", + Text = MaintenanceSettingsStrings.ImportScoresFromStable, Action = () => { importScoresButton.Enabled.Value = false; @@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Add(deleteScoresButton = new DangerousSettingsButton { - Text = "Delete ALL scores", + Text = MaintenanceSettingsStrings.DeleteAllScores, Action = () => { dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => @@ -89,7 +90,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { Add(importSkinsButton = new SettingsButton { - Text = "Import skins from stable", + Text = MaintenanceSettingsStrings.ImportSkinsFromStable, Action = () => { importSkinsButton.Enabled.Value = false; @@ -100,7 +101,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Add(deleteSkinsButton = new DangerousSettingsButton { - Text = "Delete ALL skins", + Text = MaintenanceSettingsStrings.DeleteAllSkins, Action = () => { dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => @@ -117,7 +118,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { Add(importCollectionsButton = new SettingsButton { - Text = "Import collections from stable", + Text = MaintenanceSettingsStrings.ImportCollectionsFromStable, Action = () => { importCollectionsButton.Enabled.Value = false; @@ -128,7 +129,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Add(new DangerousSettingsButton { - Text = "Delete ALL collections", + Text = MaintenanceSettingsStrings.DeleteAllCollections, Action = () => { dialogOverlay?.Push(new DeleteAllBeatmapsDialog(collectionManager.DeleteAll)); @@ -140,7 +141,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { restoreButton = new SettingsButton { - Text = "Restore all hidden difficulties", + Text = MaintenanceSettingsStrings.RestoreAllHiddenDifficulties, Action = () => { restoreButton.Enabled.Value = false; @@ -153,7 +154,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance }, undeleteButton = new SettingsButton { - Text = "Restore all recently deleted beatmaps", + Text = MaintenanceSettingsStrings.RestoreAllRecentlyDeletedBeatmaps, Action = () => { undeleteButton.Enabled.Value = false; diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs index 73c88b8e71..fa0c06167b 100644 --- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs @@ -3,6 +3,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Maintenance; using osuTK; @@ -10,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections { public class MaintenanceSection : SettingsSection { - public override string Header => "Maintenance"; + public override LocalisableString Header => MaintenanceSettingsStrings.MaintenanceSectionHeader; public override Drawable CreateIcon() => new SpriteIcon { diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs index 3a2de2ee36..351a32c72e 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs @@ -5,12 +5,13 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Online { public class AlertsAndPrivacySettings : SettingsSubsection { - protected override LocalisableString Header => "Alerts and Privacy"; + protected override LocalisableString Header => OnlineSettingsStrings.AlertsAndPrivacyHeader; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -19,12 +20,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online { new SettingsCheckbox { - LabelText = "Show a notification when someone mentions your name", + LabelText = OnlineSettingsStrings.NotifyOnMentioned, Current = config.GetBindable(OsuSetting.NotifyOnUsernameMentioned) }, new SettingsCheckbox { - LabelText = "Show a notification when you receive a private message", + LabelText = OnlineSettingsStrings.NotifyOnPrivateMessage, Current = config.GetBindable(OsuSetting.NotifyOnPrivateMessage) }, }; diff --git a/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs index f2012f0d9c..0207f2fd01 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs @@ -5,12 +5,13 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Online { public class IntegrationSettings : SettingsSubsection { - protected override LocalisableString Header => "Integrations"; + protected override LocalisableString Header => OnlineSettingsStrings.IntegrationsHeader; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -19,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Online { new SettingsEnumDropdown { - LabelText = "Discord Rich Presence", + LabelText = OnlineSettingsStrings.DiscordRichPresence, Current = config.GetBindable(OsuSetting.DiscordRichPresence) } }; diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index 89e7b096f3..e864260cc6 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -5,12 +5,13 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Online { public class WebSettings : SettingsSubsection { - protected override LocalisableString Header => "Web"; + protected override LocalisableString Header => OnlineSettingsStrings.WebHeader; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -19,24 +20,24 @@ namespace osu.Game.Overlays.Settings.Sections.Online { new SettingsCheckbox { - LabelText = "Warn about opening external links", + LabelText = OnlineSettingsStrings.ExternalLinkWarning, Current = config.GetBindable(OsuSetting.ExternalLinkWarning) }, new SettingsCheckbox { - LabelText = "Prefer downloads without video", + LabelText = OnlineSettingsStrings.PreferNoVideo, Keywords = new[] { "no-video" }, Current = config.GetBindable(OsuSetting.PreferNoVideo) }, new SettingsCheckbox { - LabelText = "Automatically download beatmaps when spectating", + LabelText = OnlineSettingsStrings.AutomaticallyDownloadWhenSpectating, Keywords = new[] { "spectator" }, Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), }, new SettingsCheckbox { - LabelText = "Show explicit content in search results", + LabelText = OnlineSettingsStrings.ShowExplicitContent, Keywords = new[] { "nsfw", "18+", "offensive" }, Current = config.GetBindable(OsuSetting.ShowOnlineExplicitContent), } diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs index 680d11f7da..8b523b90b9 100644 --- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs +++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs @@ -3,13 +3,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Online; namespace osu.Game.Overlays.Settings.Sections { public class OnlineSection : SettingsSection { - public override string Header => "Online"; + public override LocalisableString Header => OnlineSettingsStrings.OnlineSectionHeader; public override Drawable CreateIcon() => new SpriteIcon { diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 9f3543d059..e0d8252930 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -13,6 +13,7 @@ using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Skinning; using osu.Game.Skinning.Editor; using osuTK; @@ -23,7 +24,7 @@ namespace osu.Game.Overlays.Settings.Sections { private SkinSettingsDropdown skinDropdown; - public override string Header => "Skin"; + public override LocalisableString Header => SkinSettingsStrings.SkinSectionHeader; public override Drawable CreateIcon() => new SpriteIcon { @@ -69,34 +70,34 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown = new SkinSettingsDropdown(), new SettingsButton { - Text = "Skin layout editor", + Text = SkinSettingsStrings.SkinLayoutEditor, Action = () => skinEditor?.Toggle(), }, new ExportSkinButton(), new SettingsSlider { - LabelText = "Gameplay cursor size", + LabelText = SkinSettingsStrings.GameplayCursorSize, Current = config.GetBindable(OsuSetting.GameplayCursorSize), KeyboardStep = 0.01f }, new SettingsCheckbox { - LabelText = "Adjust gameplay cursor size based on current beatmap", + LabelText = SkinSettingsStrings.AutoCursorSize, Current = config.GetBindable(OsuSetting.AutoCursorSize) }, new SettingsCheckbox { - LabelText = "Beatmap skins", + LabelText = SkinSettingsStrings.BeatmapSkins, Current = config.GetBindable(OsuSetting.BeatmapSkins) }, new SettingsCheckbox { - LabelText = "Beatmap colours", + LabelText = SkinSettingsStrings.BeatmapColours, Current = config.GetBindable(OsuSetting.BeatmapColours) }, new SettingsCheckbox { - LabelText = "Beatmap hitsounds", + LabelText = SkinSettingsStrings.BeatmapHitsounds, Current = config.GetBindable(OsuSetting.BeatmapHitsounds) }, }; @@ -200,7 +201,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = "Export selected skin"; + Text = SkinSettingsStrings.ExportSkinButton; Action = export; currentSkin = skins.CurrentSkin.GetBoundCopy(); diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 4b26645ef3..0afbed5df5 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -6,12 +6,13 @@ using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.UserInterface { public class GeneralSettings : SettingsSubsection { - protected override LocalisableString Header => "General"; + protected override LocalisableString Header => UserInterfaceStrings.GeneralHeader; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -20,23 +21,23 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { new SettingsCheckbox { - LabelText = "Rotate cursor when dragging", + LabelText = UserInterfaceStrings.CursorRotation, Current = config.GetBindable(OsuSetting.CursorRotation) }, new SettingsSlider { - LabelText = "Menu cursor size", + LabelText = UserInterfaceStrings.MenuCursorSize, Current = config.GetBindable(OsuSetting.MenuCursorSize), KeyboardStep = 0.01f }, new SettingsCheckbox { - LabelText = "Parallax", + LabelText = UserInterfaceStrings.Parallax, Current = config.GetBindable(OsuSetting.MenuParallax) }, new SettingsSlider { - LabelText = "Hold-to-confirm activation time", + LabelText = UserInterfaceStrings.HoldToConfirmActivationTime, Current = config.GetBindable(OsuSetting.UIHoldActivationDelay), KeyboardStep = 50 }, diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs index 81bbcbb54a..40485a070c 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Users; @@ -13,7 +14,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { public class MainMenuSettings : SettingsSubsection { - protected override LocalisableString Header => "Main Menu"; + protected override LocalisableString Header => UserInterfaceStrings.MainMenuHeader; private IBindable user; @@ -28,27 +29,27 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { new SettingsCheckbox { - LabelText = "Interface voices", + LabelText = UserInterfaceStrings.InterfaceVoices, Current = config.GetBindable(OsuSetting.MenuVoice) }, new SettingsCheckbox { - LabelText = "osu! music theme", + LabelText = UserInterfaceStrings.OsuMusicTheme, Current = config.GetBindable(OsuSetting.MenuMusic) }, new SettingsEnumDropdown { - LabelText = "Intro sequence", + LabelText = UserInterfaceStrings.IntroSequence, Current = config.GetBindable(OsuSetting.IntroSequence), }, backgroundSourceDropdown = new SettingsEnumDropdown { - LabelText = "Background source", + LabelText = UserInterfaceStrings.BackgroundSource, Current = config.GetBindable(OsuSetting.MenuBackgroundSource), }, new SettingsEnumDropdown { - LabelText = "Seasonal backgrounds", + LabelText = UserInterfaceStrings.SeasonalBackgrounds, Current = config.GetBindable(OsuSetting.SeasonalBackgroundMode), } }; @@ -60,9 +61,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface user.BindValueChanged(u => { - const string not_supporter_note = "Changes to this setting will only apply with an active osu!supporter tag."; - - backgroundSourceDropdown.WarningText = u.NewValue?.IsSupporter != true ? not_supporter_note : string.Empty; + backgroundSourceDropdown.WarningText = u.NewValue?.IsSupporter != true ? UserInterfaceStrings.NotSupporterNote : default; }, true); } } diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index 587155eb0d..6290046987 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.UserInterface { @@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface private Bindable minStars; private Bindable maxStars; - protected override LocalisableString Header => "Song Select"; + protected override LocalisableString Header => UserInterfaceStrings.SongSelectHeader; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -31,31 +32,31 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { new SettingsCheckbox { - LabelText = "Right mouse drag to absolute scroll", + LabelText = UserInterfaceStrings.RightMouseScroll, Current = config.GetBindable(OsuSetting.SongSelectRightMouseScroll), }, new SettingsCheckbox { - LabelText = "Show converted beatmaps", + LabelText = UserInterfaceStrings.ShowConvertedBeatmaps, Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), }, new SettingsSlider { - LabelText = "Display beatmaps from", + LabelText = UserInterfaceStrings.StarsMinimum, Current = config.GetBindable(OsuSetting.DisplayStarsMinimum), KeyboardStep = 0.1f, Keywords = new[] { "minimum", "maximum", "star", "difficulty" } }, new SettingsSlider { - LabelText = "up to", + LabelText = UserInterfaceStrings.StarsMaximum, Current = config.GetBindable(OsuSetting.DisplayStarsMaximum), KeyboardStep = 0.1f, Keywords = new[] { "minimum", "maximum", "star", "difficulty" } }, new SettingsEnumDropdown { - LabelText = "Random selection algorithm", + LabelText = UserInterfaceStrings.RandomSelectionAlgorithm, Current = config.GetBindable(OsuSetting.RandomSelectAlgorithm), } }; @@ -63,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface private class MaximumStarsSlider : StarsSlider { - public override LocalisableString TooltipText => Current.IsDefault ? "no limit" : base.TooltipText; + public override LocalisableString TooltipText => Current.IsDefault ? UserInterfaceStrings.NoLimit : base.TooltipText; } private class StarsSlider : OsuSliderBar diff --git a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs index 718fea5f2b..6228c4c99a 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs @@ -3,13 +3,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.UserInterface; namespace osu.Game.Overlays.Settings.Sections { public class UserInterfaceSection : SettingsSection { - public override string Header => "User Interface"; + public override LocalisableString Header => UserInterfaceStrings.UserInterfaceSectionHeader; public override Drawable CreateIcon() => new SpriteIcon { diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index ef2027fdab..5282217013 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -65,7 +65,7 @@ namespace osu.Game.Overlays.Settings { set { - bool hasValue = string.IsNullOrWhiteSpace(value.ToString()); + bool hasValue = !string.IsNullOrWhiteSpace(value.ToString()); if (warningText == null) { @@ -76,7 +76,7 @@ namespace osu.Game.Overlays.Settings FlowContent.Add(warningText = new SettingsNoticeText(colours) { Margin = new MarginPadding { Bottom = 5 } }); } - warningText.Alpha = hasValue ? 0 : 1; + warningText.Alpha = hasValue ? 1 : 0; warningText.Text = value.ToString(); // TODO: Remove ToString() call after TextFlowContainer supports localisation (see https://github.com/ppy/osu-framework/issues/4636). } } @@ -93,15 +93,13 @@ namespace osu.Game.Overlays.Settings public bool MatchingFilter { - set => this.FadeTo(value ? 1 : 0); + set => Alpha = value ? 1 : 0; } public bool FilteringActive { get; set; } public event Action SettingChanged; - private readonly RestoreDefaultValueButton restoreDefaultButton; - protected SettingsItem() { RelativeSizeAxes = Axes.X; @@ -110,7 +108,6 @@ namespace osu.Game.Overlays.Settings InternalChildren = new Drawable[] { - restoreDefaultButton = new RestoreDefaultValueButton(), FlowContent = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -123,7 +120,7 @@ namespace osu.Game.Overlays.Settings }, }; - // all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is + // IMPORTANT: all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is // never loaded, but requires bindable storage. if (controlWithCurrent == null) throw new ArgumentException(@$"Control created via {nameof(CreateControl)} must implement {nameof(IHasCurrentValue)}"); @@ -132,12 +129,17 @@ namespace osu.Game.Overlays.Settings controlWithCurrent.Current.DisabledChanged += _ => updateDisabled(); } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - + // intentionally done before LoadComplete to avoid overhead. if (ShowsDefaultIndicator) - restoreDefaultButton.Current = controlWithCurrent.Current; + { + AddInternal(new RestoreDefaultValueButton + { + Current = controlWithCurrent.Current, + }); + } } private void updateDisabled() diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index 4143605c28..2a6f3f5ed7 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -4,9 +4,12 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK.Graphics; @@ -18,11 +21,15 @@ namespace osu.Game.Overlays.Settings protected FillFlowContainer FlowContent; protected override Container Content => FlowContent; + private IBindable selectedSection; + + private OsuSpriteText header; + public abstract Drawable CreateIcon(); - public abstract string Header { get; } + public abstract LocalisableString Header { get; } public IEnumerable FilterableChildren => Children.OfType(); - public virtual IEnumerable FilterTerms => new[] { Header }; + public virtual IEnumerable FilterTerms => new[] { Header.ToString() }; private const int header_size = 26; private const int margin = 20; @@ -35,9 +42,11 @@ namespace osu.Game.Overlays.Settings public bool FilteringActive { get; set; } + [Resolved] + private SettingsPanel settingsPanel { get; set; } + protected SettingsSection() { - Margin = new MarginPadding { Top = margin }; AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; @@ -60,6 +69,7 @@ namespace osu.Game.Overlays.Settings { new Box { + Name = "separator", Colour = new Color4(0, 0, 0, 255), RelativeSizeAxes = Axes.X, Height = border_size, @@ -69,13 +79,13 @@ namespace osu.Game.Overlays.Settings Padding = new MarginPadding { Top = margin + border_size, - Bottom = 10, + Bottom = margin + 10, }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Children = new Drawable[] { - new OsuSpriteText + header = new OsuSpriteText { Font = OsuFont.GetFont(size: header_size), Text = Header, @@ -90,6 +100,51 @@ namespace osu.Game.Overlays.Settings } }, }); + + selectedSection = settingsPanel.CurrentSection.GetBoundCopy(); + selectedSection.BindValueChanged(_ => updateContentFade(), true); + } + + private bool isCurrentSection => selectedSection.Value == this; + + protected override bool OnHover(HoverEvent e) + { + updateContentFade(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateContentFade(); + base.OnHoverLost(e); + } + + protected override bool OnClick(ClickEvent e) + { + if (!isCurrentSection) + settingsPanel.SectionsContainer.ScrollTo(this); + + return base.OnClick(e); + } + + protected override bool ShouldBeConsideredForInput(Drawable child) => + // only the current section should accept input. + // this provides the behaviour of the first click scrolling the target section to the centre of the screen. + isCurrentSection; + + private void updateContentFade() + { + float contentFade = 1; + float headerFade = 1; + + if (!isCurrentSection) + { + contentFade = 0.25f; + headerFade = IsHovered ? 0.5f : 0.25f; + } + + header.FadeTo(headerFade, 500, Easing.OutQuint); + FlowContent.FadeTo(contentFade, 500, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index df32424b67..4aa9360452 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -24,6 +24,11 @@ namespace osu.Game.Overlays.Settings protected abstract LocalisableString Header { get; } public IEnumerable FilterableChildren => Children.OfType(); + + // FilterTerms should contains both original string and localised string for user to search. + // Since LocalisableString is unable to get original string at this time (2021-08-14), + // only call .ToString() to use localised one. + // TODO: Update here when FilterTerms accept LocalisableString. public virtual IEnumerable FilterTerms => new[] { Header.ToString() }; public bool MatchingFilter diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index 30a53b351d..cf6a313a1f 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -22,6 +22,9 @@ namespace osu.Game.Overlays.Settings private readonly Box selectionIndicator; private readonly Container text; + // always consider as part of flow, even when not visible (for the sake of the initial animation). + public override bool IsPresent => true; + private SettingsSection section; public SettingsSection Section diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index f1c41c4b50..e4e76592d8 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -4,9 +4,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -20,6 +22,7 @@ using osu.Game.Overlays.Settings; namespace osu.Game.Overlays { + [Cached] public abstract class SettingsPanel : OsuFocusedOverlayContainer { public const float CONTENT_MARGINS = 15; @@ -45,7 +48,7 @@ namespace osu.Game.Overlays protected Sidebar Sidebar; private SidebarButton selectedSidebarButton; - protected SettingsSectionsContainer SectionsContainer; + public SettingsSectionsContainer SectionsContainer { get; private set; } private SeekLimitedSearchTextBox searchTextBox; @@ -58,6 +61,14 @@ namespace osu.Game.Overlays private readonly bool showSidebar; + private LoadingLayer loading; + + private readonly List loadableSections = new List(); + + private Task sectionsLoadingTask; + + public IBindable CurrentSection = new Bindable(); + protected SettingsPanel(bool showSidebar) { this.showSidebar = showSidebar; @@ -86,69 +97,49 @@ namespace osu.Game.Overlays Colour = OsuColour.Gray(0.05f), Alpha = 1, }, - SectionsContainer = new SettingsSectionsContainer + loading = new LoadingLayer { - Masking = true, - RelativeSizeAxes = Axes.Both, - ExpandableHeader = CreateHeader(), - FixedHeader = searchTextBox = new SeekLimitedSearchTextBox - { - RelativeSizeAxes = Axes.X, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Width = 0.95f, - Margin = new MarginPadding - { - Top = 20, - Bottom = 20 - }, - }, - Footer = CreateFooter() - }, + State = { Value = Visibility.Visible } + } } }; + Add(SectionsContainer = new SettingsSectionsContainer + { + Masking = true, + RelativeSizeAxes = Axes.Both, + ExpandableHeader = CreateHeader(), + SelectedSection = { BindTarget = CurrentSection }, + FixedHeader = searchTextBox = new SeekLimitedSearchTextBox + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Width = 0.95f, + Margin = new MarginPadding + { + Top = 20, + Bottom = 20 + }, + }, + Footer = CreateFooter().With(f => f.Alpha = 0) + }); + if (showSidebar) { AddInternal(Sidebar = new Sidebar { Width = sidebar_width }); - - SectionsContainer.SelectedSection.ValueChanged += section => - { - selectedSidebarButton.Selected = false; - selectedSidebarButton = Sidebar.Children.Single(b => b.Section == section.NewValue); - selectedSidebarButton.Selected = true; - }; } - searchTextBox.Current.ValueChanged += term => SectionsContainer.SearchContainer.SearchTerm = term.NewValue; - CreateSections()?.ForEach(AddSection); } protected void AddSection(SettingsSection section) { - SectionsContainer.Add(section); + if (IsLoaded) + // just to keep things simple. can be accommodated for if we ever need it. + throw new InvalidOperationException("All sections must be added before the panel is loaded."); - if (Sidebar != null) - { - var button = new SidebarButton - { - Section = section, - Action = () => - { - SectionsContainer.ScrollTo(section); - Sidebar.State = ExpandedState.Contracted; - }, - }; - - Sidebar.Add(button); - - if (selectedSidebarButton == null) - { - selectedSidebarButton = Sidebar.Children.First(); - selectedSidebarButton.Selected = true; - } - } + loadableSections.Add(section); } protected virtual Drawable CreateHeader() => new Container(); @@ -161,6 +152,12 @@ namespace osu.Game.Overlays ContentContainer.MoveToX(ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint); + // delay load enough to ensure it doesn't overlap with the initial animation. + // this is done as there is still a brief stutter during load completion which is more visible if the transition is in progress. + // the eventual goal would be to remove the need for this by splitting up load into smaller work pieces, or fixing the remaining + // load complete overheads. + Scheduler.AddDelayed(loadSections, TRANSITION_LENGTH / 3); + Sidebar?.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); this.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); @@ -199,6 +196,80 @@ namespace osu.Game.Overlays Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 }; } + private const double fade_in_duration = 1000; + + private void loadSections() + { + if (sectionsLoadingTask != null) + return; + + sectionsLoadingTask = LoadComponentsAsync(loadableSections, sections => + { + SectionsContainer.AddRange(sections); + SectionsContainer.Footer.FadeInFromZero(fade_in_duration, Easing.OutQuint); + SectionsContainer.SearchContainer.FadeInFromZero(fade_in_duration, Easing.OutQuint); + + loading.Hide(); + + searchTextBox.Current.BindValueChanged(term => SectionsContainer.SearchContainer.SearchTerm = term.NewValue, true); + searchTextBox.TakeFocus(); + + loadSidebarButtons(); + }); + } + + private void loadSidebarButtons() + { + if (Sidebar == null) + return; + + LoadComponentsAsync(createSidebarButtons(), buttons => + { + float delay = 0; + + foreach (var button in buttons) + { + Sidebar.Add(button); + + button.FadeOut() + .Delay(delay) + .FadeInFromZero(fade_in_duration, Easing.OutQuint); + + delay += 40; + } + + SectionsContainer.SelectedSection.BindValueChanged(section => + { + if (selectedSidebarButton != null) + selectedSidebarButton.Selected = false; + + selectedSidebarButton = Sidebar.Children.FirstOrDefault(b => b.Section == section.NewValue); + + if (selectedSidebarButton != null) + selectedSidebarButton.Selected = true; + }, true); + }); + } + + private IEnumerable createSidebarButtons() + { + foreach (var section in SectionsContainer) + { + yield return new SidebarButton + { + Section = section, + Action = () => + { + if (!SectionsContainer.IsLoaded) + return; + + SectionsContainer.ScrollTo(section); + Sidebar.State = ExpandedState.Contracted; + }, + }; + } + } + private class NonMaskedContent : Container { // masking breaks the pan-out transform with nested sub-settings panels. @@ -209,6 +280,16 @@ namespace osu.Game.Overlays { public SearchContainer SearchContainer; + public string SearchTerm + { + get => SearchContainer.SearchTerm; + set + { + SearchContainer.SearchTerm = value; + InvalidateScrollPosition(); + } + } + protected override FlowContainer CreateScrollContentContainer() => SearchContainer = new SearchContainer { diff --git a/osu.Game/Performance/HighPerformanceSession.cs b/osu.Game/Performance/HighPerformanceSession.cs index 661c1046f1..3ef0e0bf93 100644 --- a/osu.Game/Performance/HighPerformanceSession.cs +++ b/osu.Game/Performance/HighPerformanceSession.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Screens.Play; namespace osu.Game.Performance { @@ -12,9 +13,9 @@ namespace osu.Game.Performance private readonly IBindable localUserPlaying = new Bindable(); [BackgroundDependencyLoader] - private void load(OsuGame game) + private void load(ILocalUserPlayInfo localUserInfo) { - localUserPlaying.BindTo(game.LocalUserPlaying); + localUserPlaying.BindTo(localUserInfo.IsPlaying); } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs new file mode 100644 index 0000000000..73bab31e82 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs @@ -0,0 +1,54 @@ +// 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.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Difficulty.Skills +{ + /// + /// Used to processes strain values of s, keep track of strain levels caused by the processed objects + /// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects. + /// + public abstract class StrainDecaySkill : StrainSkill + { + /// + /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other. + /// + protected abstract double SkillMultiplier { get; } + + /// + /// Determines how quickly strain decays for the given skill. + /// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second. + /// + protected abstract double StrainDecayBase { get; } + + /// + /// The current strain level. + /// + protected double CurrentStrain { get; private set; } = 1; + + protected StrainDecaySkill(Mod[] mods) + : base(mods) + { + } + + protected override double CalculateInitialStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime); + + protected override double StrainValueAt(DifficultyHitObject current) + { + CurrentStrain *= strainDecay(current.DeltaTime); + CurrentStrain += StrainValueOf(current) * SkillMultiplier; + + return CurrentStrain; + } + + /// + /// Calculates the strain value of a . This value is affected by previously processed objects. + /// + protected abstract double StrainValueOf(DifficultyHitObject current); + + private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000); + } +} diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index d4fcefab9b..0880f1b08e 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -15,27 +15,11 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// public abstract class StrainSkill : Skill { - /// - /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other. - /// - protected abstract double SkillMultiplier { get; } - - /// - /// Determines how quickly strain decays for the given skill. - /// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second. - /// - protected abstract double StrainDecayBase { get; } - /// /// The weight by which each strain value decays. /// protected virtual double DecayWeight => 0.9; - /// - /// The current strain level. - /// - protected double CurrentStrain { get; private set; } = 1; - /// /// The length of each strain section. /// @@ -52,6 +36,11 @@ namespace osu.Game.Rulesets.Difficulty.Skills { } + /// + /// Returns the strain value at . This value is calculated with or without respect to previous objects. + /// + protected abstract double StrainValueAt(DifficultyHitObject current); + /// /// Process a and update current strain values accordingly. /// @@ -68,10 +57,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills currentSectionEnd += SectionLength; } - CurrentStrain *= strainDecay(current.DeltaTime); - CurrentStrain += StrainValueOf(current) * SkillMultiplier; - - currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak); + currentSectionPeak = Math.Max(StrainValueAt(current), currentSectionPeak); } /// @@ -88,9 +74,9 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// The beginning of the new section in milliseconds. private void startNewSectionFrom(double time) { - // The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries. + // The maximum strain of the new section is not zero by default // This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level. - currentSectionPeak = GetPeakStrain(time); + currentSectionPeak = CalculateInitialStrain(time); } /// @@ -98,7 +84,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// /// The time to retrieve the peak strain at. /// The peak strain. - protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime); + protected abstract double CalculateInitialStrain(double time); /// /// Returns a live enumerable of the peak strains for each section of the beatmap, @@ -124,12 +110,5 @@ namespace osu.Game.Rulesets.Difficulty.Skills return difficulty; } - - /// - /// Calculates the strain value of a . This value is affected by previously processed objects. - /// - protected abstract double StrainValueOf(DifficultyHitObject current); - - private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000); } } diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs index 067657159b..3978378c3a 100644 --- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs +++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mods { // Intercept and extract the internal number bindable from DifficultyBindable. // This will provide bounds and precision specifications for the slider bar. - difficultyBindable = ((DifficultyBindable)value).GetBoundCopy(); + difficultyBindable = (DifficultyBindable)value.GetBoundCopy(); sliderDisplayCurrent.BindTo(difficultyBindable.CurrentNumber); base.Current = difficultyBindable; @@ -91,7 +91,13 @@ namespace osu.Game.Rulesets.Mods { // This is required as SettingsItem relies heavily on this bindable for internal use. // The actual update flow is done via the bindable provided in the constructor. - public Bindable Current { get; set; } = new Bindable(); + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } public SliderControl(BindableNumber currentNumber) { diff --git a/osu.Game/Rulesets/Mods/DifficultyBindable.cs b/osu.Game/Rulesets/Mods/DifficultyBindable.cs index 664b88eef4..e4304795f2 100644 --- a/osu.Game/Rulesets/Mods/DifficultyBindable.cs +++ b/osu.Game/Rulesets/Mods/DifficultyBindable.cs @@ -128,6 +128,6 @@ namespace osu.Game.Rulesets.Mods ExtendedLimits.UnbindFrom(otherDifficultyBindable.ExtendedLimits); } - public new DifficultyBindable GetBoundCopy() => new DifficultyBindable { BindTarget = this }; + protected override Bindable CreateInstance() => new DifficultyBindable(); } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f2fd02c652..9f3b5eaf5b 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -129,6 +129,17 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual Type[] IncompatibleMods => Array.Empty(); + private IReadOnlyList settingsBacking; + + /// + /// A list of the all settings within this mod. + /// + internal IReadOnlyList Settings => + settingsBacking ??= this.GetSettingsSourceProperties() + .Select(p => p.Item2.GetValue(this)) + .Cast() + .ToList(); + /// /// Creates a copy of this initialised to a default state. /// @@ -191,15 +202,39 @@ namespace osu.Game.Rulesets.Mods if (ReferenceEquals(this, other)) return true; return GetType() == other.GetType() && - this.GetSettingsSourceProperties().All(pair => - EqualityComparer.Default.Equals( - ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(this)), - ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(other)))); + Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + + hashCode.Add(GetType()); + + foreach (var setting in Settings) + hashCode.Add(ModUtils.GetSettingUnderlyingValue(setting)); + + return hashCode.ToHashCode(); } /// /// Reset all custom settings for this mod back to their defaults. /// public virtual void ResetSettingsToDefaults() => CopyFrom((Mod)Activator.CreateInstance(GetType())); + + private class ModSettingsEqualityComparer : IEqualityComparer + { + public static ModSettingsEqualityComparer Default { get; } = new ModSettingsEqualityComparer(); + + public bool Equals(IBindable x, IBindable y) + { + object xValue = x == null ? null : ModUtils.GetSettingUnderlyingValue(x); + object yValue = y == null ? null : ModUtils.GetSettingUnderlyingValue(y); + + return EqualityComparer.Default.Equals(xValue, yValue); + } + + public int GetHashCode(IBindable obj) => ModUtils.GetSettingUnderlyingValue(obj).GetHashCode(); + } } } diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs index ff33f0c70d..dfe2992a7c 100644 --- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs +++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs @@ -41,6 +41,8 @@ namespace osu.Game.Screens.Edit protected override int DefaultMaxValue => VALID_DIVISORS.Last(); protected override int DefaultPrecision => 1; + protected override Bindable CreateInstance() => new BindableBeatDivisor(); + /// /// Retrieves the appropriate colour for a beat divisor. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 6e57b8e88c..911c9fea51 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -166,14 +166,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } if (IsSelected) - { border.Show(); - colour = colour.Lighten(0.3f); - } else - { border.Hide(); - } if (Item is IHasDuration duration && duration.Duration > 0) circle.Colour = ColourInfo.GradientHorizontal(colour, colour.Lighten(0.4f)); @@ -212,14 +207,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline for (int i = 0; i < repeats.RepeatCount; i++) { - repeatsContainer.Add(new Circle + repeatsContainer.Add(new Tick { - Size = new Vector2(circle_size / 3), - Alpha = 0.2f, - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = (float)(i + 1) / (repeats.RepeatCount + 1), + X = (float)(i + 1) / (repeats.RepeatCount + 1) }); } } @@ -233,6 +223,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft; + private class Tick : Circle + { + public Tick() + { + Size = new Vector2(circle_size / 4); + Anchor = Anchor.CentreLeft; + Origin = Anchor.Centre; + RelativePositionAxes = Axes.X; + } + } + public class DragArea : Circle { private readonly HitObject hitObject; @@ -304,20 +305,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateState() { - if (hasMouseDown) - { - this.ScaleTo(0.7f, 200, Easing.OutQuint); - } - else if (IsHovered) - { - this.ScaleTo(0.8f, 200, Easing.OutQuint); - } - else - { - this.ScaleTo(0.6f, 200, Easing.OutQuint); - } + float scale = 0.5f; - this.FadeTo(IsHovered || hasMouseDown ? 0.8f : 0.2f, 200, Easing.OutQuint); + if (hasMouseDown) + scale = 0.6f; + else if (IsHovered) + scale = 0.7f; + + this.ScaleTo(scale, 200, Easing.OutQuint); + this.FadeTo(IsHovered || hasMouseDown ? 1f : 0.9f, 200, Easing.OutQuint); } [Resolved] diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 772f6ea192..ba83261731 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -150,8 +150,6 @@ namespace osu.Game.Screens.Edit if (seekTime < timingPoint.Time && timingPoint != ControlPointInfo.TimingPoints.First()) seekTime = timingPoint.Time; - // Ensure the sought point is within the boundaries - seekTime = Math.Clamp(seekTime, 0, TrackLength); SeekSmoothlyTo(seekTime); } @@ -190,6 +188,9 @@ namespace osu.Game.Screens.Edit seekingOrStopped.Value = IsSeeking = true; ClearTransforms(); + + // Ensure the sought point is within the boundaries + position = Math.Clamp(position, 0, TrackLength); return underlyingClock.Seek(position); } @@ -288,7 +289,7 @@ namespace osu.Game.Screens.Edit } private void transformSeekTo(double seek, double duration = 0, Easing easing = Easing.None) - => this.TransformTo(this.PopulateTransform(new TransformSeek(), seek, duration, easing)); + => this.TransformTo(this.PopulateTransform(new TransformSeek(), Math.Clamp(seek, 0, TrackLength), duration, easing)); private double currentTime { diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 71f3b60026..07a94fb97e 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.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.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -109,7 +110,7 @@ namespace osu.Game.Screens.Menu bool loadThemedIntro() { - setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash); + setInfo = beatmaps.QueryBeatmapSets(b => b.Hash == BeatmapHash, IncludedDetails.AllButRuleset).FirstOrDefault(); if (setInfo == null) return false; diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index e50784fcbe..1387b5a671 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.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.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -14,8 +15,10 @@ namespace osu.Game.Screens.OnlinePlay.Components /// public class ListingPollingComponent : RoomPollingComponent { - [Resolved] - private Bindable currentFilter { get; set; } + public IBindable InitialRoomsReceived => initialRoomsReceived; + private readonly Bindable initialRoomsReceived = new Bindable(); + + public readonly Bindable Filter = new Bindable(); [Resolved] private Bindable selectedRoom { get; set; } @@ -23,9 +26,11 @@ namespace osu.Game.Screens.OnlinePlay.Components [BackgroundDependencyLoader] private void load() { - currentFilter.BindValueChanged(_ => + Filter.BindValueChanged(_ => { - NotifyRoomsReceived(null); + RoomManager.ClearRooms(); + initialRoomsReceived.Value = false; + if (IsLoaded) PollImmediately(); }); @@ -38,24 +43,26 @@ namespace osu.Game.Screens.OnlinePlay.Components if (!API.IsLoggedIn) return base.Poll(); + if (Filter.Value == null) + return base.Poll(); + var tcs = new TaskCompletionSource(); pollReq?.Cancel(); - pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category); + pollReq = new GetRoomsRequest(Filter.Value.Status, Filter.Value.Category); pollReq.Success += result => { - for (int i = 0; i < result.Count; i++) + foreach (var existing in RoomManager.Rooms.ToArray()) { - if (result[i].RoomID.Value == selectedRoom.Value?.RoomID.Value) - { - // The listing request always has less information than the opened room, so don't include it. - result[i] = selectedRoom.Value; - break; - } + if (result.All(r => r.RoomID.Value != existing.RoomID.Value)) + RoomManager.RemoveRoom(existing); } - NotifyRoomsReceived(result); + foreach (var incoming in result) + RoomManager.AddOrUpdateRoom(incoming); + + initialRoomsReceived.Value = true; tcs.SetResult(true); }; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 422576648c..3b6c1c8be0 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -8,7 +8,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online.API; @@ -17,15 +16,12 @@ using osu.Game.Rulesets; namespace osu.Game.Screens.OnlinePlay.Components { - public abstract class RoomManager : CompositeDrawable, IRoomManager + public class RoomManager : Component, IRoomManager { public event Action RoomsUpdated; private readonly BindableList rooms = new BindableList(); - public IBindable InitialRoomsReceived => initialRoomsReceived; - private readonly Bindable initialRoomsReceived = new Bindable(); - public IBindableList Rooms => rooms; protected IBindable JoinedRoom => joinedRoom; @@ -40,15 +36,9 @@ namespace osu.Game.Screens.OnlinePlay.Components [Resolved] private IAPIProvider api { get; set; } - protected RoomManager() + public RoomManager() { RelativeSizeAxes = Axes.Both; - - InternalChildren = CreatePollingComponents().Select(p => - { - p.RoomsReceived = onRoomsReceived; - return p; - }).ToList(); } protected override void Dispose(bool isDisposing) @@ -67,10 +57,10 @@ namespace osu.Game.Screens.OnlinePlay.Components { joinedRoom.Value = room; - update(room, result); - addRoom(room); + AddOrUpdateRoom(result); + room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere. - RoomsUpdated?.Invoke(); + // The server may not contain all properties (such as password), so invoke success with the given room. onSuccess?.Invoke(room); }; @@ -118,84 +108,49 @@ namespace osu.Game.Screens.OnlinePlay.Components private readonly HashSet ignoredRooms = new HashSet(); - private void onRoomsReceived(List received) + public void AddOrUpdateRoom(Room room) { - if (received == null) - { - ClearRooms(); + Debug.Assert(room.RoomID.Value != null); + + if (ignoredRooms.Contains(room.RoomID.Value.Value)) return; - } - // Remove past matches - foreach (var r in rooms.ToList()) + room.Position.Value = -room.RoomID.Value.Value; + + try { - if (received.All(e => e.RoomID.Value != r.RoomID.Value)) - rooms.Remove(r); - } + foreach (var pi in room.Playlist) + pi.MapObjects(beatmaps, rulesets); - for (int i = 0; i < received.Count; i++) + var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); + if (existing == null) + rooms.Add(room); + else + existing.CopyFrom(room); + } + catch (Exception ex) { - var room = received[i]; + Logger.Error(ex, $"Failed to update room: {room.Name.Value}."); - Debug.Assert(room.RoomID.Value != null); - - if (ignoredRooms.Contains(room.RoomID.Value.Value)) - continue; - - room.Position.Value = i; - - try - { - update(room, room); - addRoom(room); - } - catch (Exception ex) - { - Logger.Error(ex, $"Failed to update room: {room.Name.Value}."); - - ignoredRooms.Add(room.RoomID.Value.Value); - rooms.Remove(room); - } + ignoredRooms.Add(room.RoomID.Value.Value); + rooms.Remove(room); } - RoomsUpdated?.Invoke(); - initialRoomsReceived.Value = true; + notifyRoomsUpdated(); } - protected void RemoveRoom(Room room) => rooms.Remove(room); + public void RemoveRoom(Room room) + { + rooms.Remove(room); + notifyRoomsUpdated(); + } - protected void ClearRooms() + public void ClearRooms() { rooms.Clear(); - initialRoomsReceived.Value = false; + notifyRoomsUpdated(); } - /// - /// Updates a local with a remote copy. - /// - /// The local to update. - /// The remote to update with. - private void update(Room local, Room remote) - { - foreach (var pi in remote.Playlist) - pi.MapObjects(beatmaps, rulesets); - - local.CopyFrom(remote); - } - - /// - /// Adds a to the list of available rooms. - /// - /// The to add. - private void addRoom(Room room) - { - var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); - if (existing == null) - rooms.Add(room); - else - existing.CopyFrom(room); - } - - protected abstract IEnumerable CreatePollingComponents(); + private void notifyRoomsUpdated() => Scheduler.AddOnce(() => RoomsUpdated?.Invoke()); } } diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs index b2ea3a05d6..cd224a7347 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs @@ -1,29 +1,18 @@ // 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.Game.Online; using osu.Game.Online.API; -using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { public abstract class RoomPollingComponent : PollingComponent { - /// - /// Invoked when any s have been received from the API. - /// - /// Any s present locally but not returned by this event are to be removed from display. - /// If null, the display of local rooms is reset to an initial state. - /// - /// - public Action> RoomsReceived; - [Resolved] protected IAPIProvider API { get; private set; } - protected void NotifyRoomsReceived(List rooms) => RoomsReceived?.Invoke(rooms); + [Resolved] + protected IRoomManager RoomManager { get; private set; } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index dcf3c94b76..88d9469f8c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -48,17 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Components pollReq.Success += result => { - // existing rooms need to be ordered by their position because the received of NotifyRoomsReceives expects to be able to sort them based on this order. - var rooms = new List(roomManager.Rooms.OrderBy(r => r.Position.Value)); - - int index = rooms.FindIndex(r => r.RoomID.Value == result.RoomID.Value); - - if (index < 0) - return; - - rooms[index] = result; - - NotifyRoomsReceived(rooms); + RoomManager.AddOrUpdateRoom(result); tcs.SetResult(true); }; diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index a27b27b8ad..7b14acf924 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -10,8 +10,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; -using osu.Game.Screens.Ranking.Expanded; using osuTK; namespace osu.Game.Screens.OnlinePlay.Components @@ -64,8 +64,8 @@ namespace osu.Game.Screens.OnlinePlay.Components AutoSizeAxes = Axes.Both, Children = new Drawable[] { - minDisplay = new StarRatingDisplay(default), - maxDisplay = new StarRatingDisplay(default) + minDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range), + maxDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range) } } }; diff --git a/osu.Game/Screens/OnlinePlay/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs index 34c1393ff1..6e1ffbda74 100644 --- a/osu.Game/Screens/OnlinePlay/IRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/IRoomManager.cs @@ -18,16 +18,29 @@ namespace osu.Game.Screens.OnlinePlay /// event Action RoomsUpdated; - /// - /// Whether an initial listing of rooms has been received. - /// - IBindable InitialRoomsReceived { get; } - /// /// All the active s. /// IBindableList Rooms { get; } + /// + /// Adds a to this . + /// If already existing, the local room will be updated with the given one. + /// + /// The incoming . + void AddOrUpdateRoom(Room room); + + /// + /// Removes a from this . + /// + /// The to remove. + void RemoveRoom(Room room); + + /// + /// Removes all s from this . + /// + void ClearRooms(); + /// /// Creates a new . /// diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index c8ecd65574..106211c833 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -1,32 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using osu.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; @@ -35,105 +22,26 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class DrawableRoom : OsuClickableContainer, IStateful, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler + public class DrawableRoom : CompositeDrawable { - public const float SELECTION_BORDER_WIDTH = 4; - private const float corner_radius = 10; - private const float transition_duration = 60; + protected const float CORNER_RADIUS = 10; private const float height = 100; - public event Action StateChanged; - - protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); - - private Drawable selectionBox; - - [Resolved(canBeNull: true)] - private LoungeSubScreen loungeScreen { get; set; } + public readonly Room Room; [Resolved] private BeatmapManager beatmaps { get; set; } - [Resolved(canBeNull: true)] - private Bindable selectedRoom { get; set; } - - [Resolved(canBeNull: true)] - private LoungeSubScreen lounge { get; set; } - - public readonly Room Room; - - private SelectionState state; - - private Sample sampleSelect; - private Sample sampleJoin; - - public SelectionState State - { - get => state; - set - { - if (value == state) - return; - - state = value; - - if (selectionBox != null) - { - if (state == SelectionState.Selected) - selectionBox.FadeIn(transition_duration); - else - selectionBox.FadeOut(transition_duration); - } - - StateChanged?.Invoke(State); - } - } - - public IEnumerable FilterTerms => new[] { Room.Name.Value }; - - private bool matchingFilter; - - public bool MatchingFilter - { - get => matchingFilter; - set - { - matchingFilter = value; - - if (!IsLoaded) - return; - - if (matchingFilter) - this.FadeIn(200); - else - Hide(); - } - } - - private int numberOfAvatars = 7; - - public int NumberOfAvatars - { - get => numberOfAvatars; - set - { - numberOfAvatars = value; - - if (recentParticipantsList != null) - recentParticipantsList.NumberOfCircles = value; - } - } + protected Container ButtonsContainer { get; private set; } + private readonly Bindable roomType = new Bindable(); private readonly Bindable roomCategory = new Bindable(); + private readonly Bindable hasPassword = new Bindable(); private RecentParticipantsList recentParticipantsList; private RoomSpecialCategoryPill specialCategoryPill; - - public bool FilteringActive { get; set; } - private PasswordProtectedIcon passwordIcon; - - private readonly Bindable hasPassword = new Bindable(); + private EndDateInfo endDateInfo; public DrawableRoom(Room room) { @@ -143,7 +51,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Height = height; Masking = true; - CornerRadius = corner_radius + SELECTION_BORDER_WIDTH / 2; + CornerRadius = CORNER_RADIUS; EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, @@ -153,9 +61,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colours, AudioManager audio) + private void load(OverlayColourProvider colours) { - Children = new Drawable[] + InternalChildren = new[] { // This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites. new Box @@ -163,10 +71,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Colour = colours.Background5, }, - new OnlinePlayBackgroundSprite + CreateBackground().With(d => { - RelativeSizeAxes = Axes.Both - }, + d.RelativeSizeAxes = Axes.Both; + }), new Container { Name = @"Room content", @@ -177,7 +85,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { RelativeSizeAxes = Axes.Both, Masking = true, - CornerRadius = corner_radius, + CornerRadius = CORNER_RADIUS, Children = new Drawable[] { new GridContainer @@ -238,10 +146,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, - new EndDateInfo + endDateInfo = new EndDateInfo { Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft + Origin = Anchor.CentreLeft, }, } }, @@ -289,13 +197,21 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Origin = Anchor.CentreRight, AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, + Spacing = new Vector2(5), Padding = new MarginPadding { Right = 10, - Vertical = 5 + Vertical = 20, }, Children = new Drawable[] { + ButtonsContainer = new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X + }, recentParticipantsList = new RecentParticipantsList { Anchor = Anchor.CentreRight, @@ -308,36 +224,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }, }, }, - new StatusColouredContainer(transition_duration) - { - RelativeSizeAxes = Axes.Both, - Child = selectionBox = new Container - { - RelativeSizeAxes = Axes.Both, - Alpha = state == SelectionState.Selected ? 1 : 0, - Masking = true, - CornerRadius = corner_radius, - BorderThickness = SELECTION_BORDER_WIDTH, - BorderColour = Color4.White, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - }, - }; - - sampleSelect = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); - sampleJoin = audio.Samples.Get($@"UI/{HoverSampleSet.Submit.GetDescription()}-select"); - } - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - return new CachedModelDependencyContainer(base.CreateChildDependencies(parent)) - { - Model = { Value = Room } }; } @@ -345,11 +231,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { base.LoadComplete(); - if (matchingFilter) - this.FadeInFromZero(transition_duration); - else - Alpha = 0; - roomCategory.BindTo(Room.Category); roomCategory.BindValueChanged(c => { @@ -359,62 +240,39 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components specialCategoryPill.Hide(); }, true); + roomType.BindTo(Room.Type); + roomType.BindValueChanged(t => + { + endDateInfo.Alpha = t.NewValue == MatchType.Playlists ? 1 : 0; + }, true); + hasPassword.BindTo(Room.HasPassword); hasPassword.BindValueChanged(v => passwordIcon.Alpha = v.NewValue ? 1 : 0, true); } - public Popover GetPopover() => new PasswordEntryPopover(Room) { JoinRequested = lounge.Join }; - - public MenuItem[] ContextMenuItems => new MenuItem[] + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - new OsuMenuItem("Create copy", MenuItemType.Standard, () => + return new CachedModelDependencyContainer(base.CreateChildDependencies(parent)) { - lounge?.Open(Room.DeepClone()); - }) - }; - - public bool OnPressed(GlobalAction action) - { - if (selectedRoom.Value != Room) - return false; - - switch (action) - { - case GlobalAction.Select: - TriggerClick(); - return true; - } - - return false; + Model = { Value = Room } + }; } - public void OnReleased(GlobalAction action) + private int numberOfAvatars = 7; + + public int NumberOfAvatars { + get => numberOfAvatars; + set + { + numberOfAvatars = value; + + if (recentParticipantsList != null) + recentParticipantsList.NumberOfCircles = value; + } } - protected override bool ShouldBeConsideredForInput(Drawable child) => state == SelectionState.Selected || child is HoverSounds; - - protected override bool OnClick(ClickEvent e) - { - if (Room != selectedRoom.Value) - { - sampleSelect?.Play(); - selectedRoom.Value = Room; - return true; - } - - if (Room.HasPassword.Value) - { - sampleJoin?.Play(); - this.ShowPopover(); - return true; - } - - sampleJoin?.Play(); - lounge?.Join(Room, null); - - return base.OnClick(e); - } + protected virtual Drawable CreateBackground() => new OnlinePlayBackgroundSprite(); private class RoomNameText : OsuSpriteText { @@ -500,52 +358,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }; } } - - public class PasswordEntryPopover : OsuPopover - { - private readonly Room room; - - public Action JoinRequested; - - public PasswordEntryPopover(Room room) - { - this.room = room; - } - - private OsuPasswordTextBox passwordTextbox; - - [BackgroundDependencyLoader] - private void load() - { - Child = new FillFlowContainer - { - Margin = new MarginPadding(10), - Spacing = new Vector2(5), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - passwordTextbox = new OsuPasswordTextBox - { - Width = 200, - }, - new TriangleButton - { - Width = 80, - Text = "Join Room", - Action = () => JoinRequested?.Invoke(room, passwordTextbox.Text) - } - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextbox)); - passwordTextbox.OnCommit += (_, __) => JoinRequested?.Invoke(room, passwordTextbox.Text); - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 46d9850fde..be5558ed0b 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -15,7 +15,6 @@ using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Game.Extensions; using osu.Game.Graphics.Cursor; -using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; using osuTK; @@ -26,12 +25,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { private readonly IBindableList rooms = new BindableList(); - private readonly FillFlowContainer roomFlow; + private readonly FillFlowContainer roomFlow; public IReadOnlyList Rooms => roomFlow.FlowingChildren.Cast().ToArray(); - [Resolved(CanBeNull = true)] - private Bindable filter { get; set; } + public readonly Bindable Filter = new Bindable(); [Resolved] private Bindable selectedRoom { get; set; } @@ -57,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = roomFlow = new FillFlowContainer + Child = roomFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -74,18 +72,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components rooms.BindTo(roomManager.Rooms); - filter?.BindValueChanged(criteria => Filter(criteria.NewValue)); - - selectedRoom.BindValueChanged(selection => - { - updateSelection(); - }, true); + Filter?.BindValueChanged(criteria => applyFilterCriteria(criteria.NewValue), true); } - private void updateSelection() => - roomFlow.Children.ForEach(r => r.State = r.Room == selectedRoom.Value ? SelectionState.Selected : SelectionState.NotSelected); - - public void Filter(FilterCriteria criteria) + private void applyFilterCriteria(FilterCriteria criteria) { roomFlow.Children.ForEach(r => { @@ -123,22 +113,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { foreach (var room in rooms) { - roomFlow.Add(new DrawableRoom(room)); + roomFlow.Add(new DrawableLoungeRoom(room)); } - Filter(filter?.Value); - - updateSelection(); + applyFilterCriteria(Filter?.Value); } private void removeRooms(IEnumerable rooms) { foreach (var r in rooms) { - var toRemove = roomFlow.Single(d => d.Room == r); - toRemove.Action = null; - - roomFlow.Remove(toRemove); + roomFlow.RemoveAll(d => d.Room == r); // selection may have a lease due to being in a sub screen. if (!selectedRoom.Disabled) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs new file mode 100644 index 0000000000..70342680e2 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -0,0 +1,225 @@ +// 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.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Lounge +{ + /// + /// A with lounge-specific interactions such as selection and hover sounds. + /// + public class DrawableLoungeRoom : DrawableRoom, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler + { + private const float transition_duration = 60; + private const float selection_border_width = 4; + + [Resolved(canBeNull: true)] + private LoungeSubScreen lounge { get; set; } + + [Resolved(canBeNull: true)] + private Bindable selectedRoom { get; set; } + + private Sample sampleSelect; + private Sample sampleJoin; + private Drawable selectionBox; + + public DrawableLoungeRoom(Room room) + : base(room) + { + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleSelect = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); + sampleJoin = audio.Samples.Get($@"UI/{HoverSampleSet.Submit.GetDescription()}-select"); + + AddRangeInternal(new Drawable[] + { + new StatusColouredContainer(transition_duration) + { + RelativeSizeAxes = Axes.Both, + Child = selectionBox = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Masking = true, + CornerRadius = CORNER_RADIUS, + BorderThickness = selection_border_width, + BorderColour = Color4.White, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + }, + new HoverSounds() + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (matchingFilter) + this.FadeInFromZero(transition_duration); + else + Alpha = 0; + + selectedRoom.BindValueChanged(updateSelectedRoom, true); + } + + private void updateSelectedRoom(ValueChangedEvent selected) + { + if (selected.NewValue == Room) + selectionBox.FadeIn(transition_duration); + else + selectionBox.FadeOut(transition_duration); + } + + public bool FilteringActive { get; set; } + + public IEnumerable FilterTerms => new[] { Room.Name.Value }; + + private bool matchingFilter; + + public bool MatchingFilter + { + get => matchingFilter; + set + { + matchingFilter = value; + + if (!IsLoaded) + return; + + if (matchingFilter) + this.FadeIn(200); + else + Hide(); + } + } + + public Popover GetPopover() => new PasswordEntryPopover(Room) { JoinRequested = lounge.Join }; + + public MenuItem[] ContextMenuItems => new MenuItem[] + { + new OsuMenuItem("Create copy", MenuItemType.Standard, () => + { + lounge?.Open(Room.DeepClone()); + }) + }; + + public bool OnPressed(GlobalAction action) + { + if (selectedRoom.Value != Room) + return false; + + switch (action) + { + case GlobalAction.Select: + TriggerClick(); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } + + protected override bool ShouldBeConsideredForInput(Drawable child) => selectedRoom.Value == Room || child is HoverSounds; + + protected override bool OnClick(ClickEvent e) + { + if (Room != selectedRoom.Value) + { + sampleSelect?.Play(); + selectedRoom.Value = Room; + return true; + } + + if (Room.HasPassword.Value) + { + sampleJoin?.Play(); + this.ShowPopover(); + return true; + } + + sampleJoin?.Play(); + lounge?.Join(Room, null); + return true; + } + + public class PasswordEntryPopover : OsuPopover + { + private readonly Room room; + + public Action JoinRequested; + + public PasswordEntryPopover(Room room) + { + this.room = room; + } + + private OsuPasswordTextBox passwordTextbox; + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Margin = new MarginPadding(10), + Spacing = new Vector2(5), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + passwordTextbox = new OsuPasswordTextBox + { + Width = 200, + }, + new TriangleButton + { + Width = 80, + Text = "Join Room", + Action = () => JoinRequested?.Invoke(room, passwordTextbox.Text) + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextbox)); + passwordTextbox.OnCommit += (_, __) => JoinRequested?.Invoke(room, passwordTextbox.Text); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index e0e5cc415e..0664e44a73 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -13,14 +13,17 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Input; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; @@ -42,10 +45,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge AutoSizeAxes = Axes.Both }; - private readonly IBindable initialRoomsReceived = new Bindable(); - private readonly IBindable operationInProgress = new Bindable(); - - private LoadingLayer loadingLayer; + protected ListingPollingComponent ListingPollingComponent { get; private set; } [Resolved] private Bindable selectedRoom { get; set; } @@ -56,108 +56,103 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved(CanBeNull = true)] private OngoingOperationTracker ongoingOperationTracker { get; set; } - [Resolved(CanBeNull = true)] - private Bindable filter { get; set; } - [Resolved] private IBindable ruleset { get; set; } [CanBeNull] private IDisposable joiningRoomOperation { get; set; } + [CanBeNull] + private LeasedBindable selectionLease; + + private readonly Bindable filter = new Bindable(new FilterCriteria()); + private readonly IBindable operationInProgress = new Bindable(); + private readonly IBindable isIdle = new BindableBool(); + private LoadingLayer loadingLayer; private RoomsContainer roomsContainer; private SearchTextBox searchTextBox; private Dropdown statusDropdown; - [CanBeNull] - private LeasedBindable selectionLease; - - [BackgroundDependencyLoader] - private void load() + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] IdleTracker idleTracker) { - filter ??= new Bindable(new FilterCriteria()); + const float controls_area_height = 25f; + + if (idleTracker != null) + isIdle.BindTo(idleTracker.IsIdle); OsuScrollContainer scrollContainer; - InternalChildren = new[] + InternalChildren = new Drawable[] { - loadingLayer = new LoadingLayer(true), + ListingPollingComponent = CreatePollingComponent().With(c => c.Filter.BindTarget = filter), new Container { + Name = @"Rooms area", RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { - Left = WaveOverlayContainer.WIDTH_PADDING, - Right = WaveOverlayContainer.WIDTH_PADDING, + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Top = Header.HEIGHT + controls_area_height + 20, }, - Child = new GridContainer + Child = scrollContainer = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + ScrollbarOverlapsContent = false, + Child = roomsContainer = new RoomsContainer { - new Dimension(GridSizeMode.Absolute, Header.HEIGHT), - new Dimension(GridSizeMode.Absolute, 25), - new Dimension(GridSizeMode.Absolute, 20) + Filter = { BindTarget = filter } + } + }, + }, + loadingLayer = new LoadingLayer(true), + new FillFlowContainer + { + Name = @"Header area flow", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Height = Header.HEIGHT, + Child = searchTextBox = new LoungeSearchTextBox + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + Width = 0.6f, + }, }, - Content = new[] + new Container { - new Drawable[] + RelativeSizeAxes = Axes.X, + Height = controls_area_height, + Children = new Drawable[] { - searchTextBox = new LoungeSearchTextBox + Buttons.WithChild(CreateNewRoomButton().With(d => { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.X, - Width = 0.6f, - }, - }, - new Drawable[] - { - new Container + d.Anchor = Anchor.BottomLeft; + d.Origin = Anchor.BottomLeft; + d.Size = new Vector2(150, 37.5f); + d.Action = () => Open(); + })), + new FillFlowContainer { - RelativeSizeAxes = Axes.Both, - Depth = float.MinValue, // Contained filters should appear over the top of rooms. - Children = new Drawable[] + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d => { - Buttons.WithChild(CreateNewRoomButton().With(d => - { - d.Anchor = Anchor.BottomLeft; - d.Origin = Anchor.BottomLeft; - d.Size = new Vector2(150, 37.5f); - d.Action = () => Open(); - })), - new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10), - ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d => - { - d.Anchor = Anchor.TopRight; - d.Origin = Anchor.TopRight; - })) - } - } + d.Anchor = Anchor.TopRight; + d.Origin = Anchor.TopRight; + })) } - }, - null, - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - scrollContainer = new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarOverlapsContent = false, - Child = roomsContainer = new RoomsContainer() - }, - } - }, } } }, @@ -180,21 +175,22 @@ namespace osu.Game.Screens.OnlinePlay.Lounge searchTextBox.Current.BindValueChanged(_ => updateFilterDebounced()); ruleset.BindValueChanged(_ => UpdateFilter()); - initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived); - initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer()); + isIdle.BindValueChanged(_ => updatePollingRate(this.IsCurrentScreen()), true); if (ongoingOperationTracker != null) { operationInProgress.BindTo(ongoingOperationTracker.InProgress); - operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true); + operationInProgress.BindValueChanged(_ => updateLoadingLayer()); } + ListingPollingComponent.InitialRoomsReceived.BindValueChanged(_ => updateLoadingLayer(), true); + updateFilter(); } #region Filtering - protected void UpdateFilter() => Scheduler.AddOnce(updateFilter); + public void UpdateFilter() => Scheduler.AddOnce(updateFilter); private ScheduledDelegate scheduledFilterUpdate; @@ -235,7 +231,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public override void OnEntering(IScreen last) { base.OnEntering(last); - onReturning(); } @@ -275,11 +270,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onReturning() { + updatePollingRate(true); searchTextBox.HoldFocus = true; } private void onLeaving() { + updatePollingRate(false); searchTextBox.HoldFocus = false; // ensure any password prompt is dismissed. @@ -327,6 +324,24 @@ namespace osu.Game.Screens.OnlinePlay.Lounge this.Push(CreateRoomSubScreen(room)); } + private void updateLoadingLayer() + { + if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value) + loadingLayer.Show(); + else + loadingLayer.Hide(); + } + + private void updatePollingRate(bool isCurrentScreen) + { + if (!isCurrentScreen) + ListingPollingComponent.TimeBetweenPolls.Value = 0; + else + ListingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; + + Logger.Log($"Polling adjusted (listing: {ListingPollingComponent.TimeBetweenPolls.Value})"); + } + protected abstract OsuButton CreateNewRoomButton(); /// @@ -337,13 +352,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected abstract RoomSubScreen CreateRoomSubScreen(Room room); - private void updateLoadingLayer() - { - if (operationInProgress.Value || !initialRoomsReceived.Value) - loadingLayer.Show(); - else - loadingLayer.Hide(); - } + protected abstract ListingPollingComponent CreatePollingComponent(); private class LoungeSearchTextBox : SearchTextBox { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs b/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs deleted file mode 100644 index e91c46beed..0000000000 --- a/osu.Game/Screens/OnlinePlay/Match/Components/Footer.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 System; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Screens.OnlinePlay.Playlists; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Match.Components -{ - public class Footer : CompositeDrawable - { - public const float HEIGHT = 50; - - public Action OnStart; - - private readonly Drawable background; - - public Footer() - { - RelativeSizeAxes = Axes.X; - Height = HEIGHT; - - InternalChildren = new[] - { - background = new Box { RelativeSizeAxes = Axes.Both }, - new PlaylistsReadyButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(600, 50), - Action = () => OnStart?.Invoke() - } - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - background.Colour = Color4Extensions.FromHex(@"28242d"); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs b/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs deleted file mode 100644 index a2d11c54c1..0000000000 --- a/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs +++ /dev/null @@ -1,79 +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.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Users.Drawables; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Match.Components -{ - public class Header : OnlinePlayComposite - { - public const float HEIGHT = 50; - - private UpdateableAvatar avatar; - private LinkFlowContainer hostText; - - public Header() - { - RelativeSizeAxes = Axes.X; - Height = HEIGHT; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - avatar = new UpdateableAvatar - { - Size = new Vector2(50), - Masking = true, - CornerRadius = 10, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(size: 30), - Current = { BindTarget = RoomName } - }, - hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 20)) - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - } - } - } - } - }; - - Host.BindValueChanged(host => - { - avatar.User = host.NewValue; - - hostText.Clear(); - - if (host.NewValue != null) - { - hostText.AddText("hosted by "); - hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); - } - }, true); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs index a96d64cb5d..5f960c1b5c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs @@ -19,9 +19,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components [Resolved(CanBeNull = true)] private ChannelManager channelManager { get; set; } - public MatchChatDisplay() + private readonly bool leaveChannelOnDispose; + + public MatchChatDisplay(bool leaveChannelOnDispose = true) : base(true) { + this.leaveChannelOnDispose = leaveChannelOnDispose; } protected override void LoadComplete() @@ -42,7 +45,9 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - channelManager?.LeaveChannel(Channel.Value); + + if (leaveChannelOnDispose) + channelManager?.LeaveChannel(Channel.Value); } } } diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs similarity index 89% rename from osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs index 2676453a7e..80a22880d4 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs @@ -14,10 +14,10 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Match.Components { - public abstract class MatchSettingsOverlay : FocusedOverlayContainer, IKeyBindingHandler + public abstract class RoomSettingsOverlay : FocusedOverlayContainer, IKeyBindingHandler { protected const float TRANSITION_DURATION = 350; - protected const float FIELD_PADDING = 45; + protected const float FIELD_PADDING = 25; protected OnlinePlayComposite Settings { get; set; } @@ -27,11 +27,16 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected abstract bool IsLoading { get; } + protected RoomSettingsOverlay() + { + RelativeSizeAxes = Axes.Both; + Masking = true; + CornerRadius = 10; + } + [BackgroundDependencyLoader] private void load() { - Masking = true; - Add(Settings = CreateSettings()); } @@ -41,12 +46,16 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected override void PopIn() { + base.PopIn(); Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint); + Settings.FadeIn(TRANSITION_DURATION / 2); } protected override void PopOut() { + base.PopOut(); Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine); + Settings.Delay(TRANSITION_DURATION / 2).FadeOut(TRANSITION_DURATION / 2); } public bool OnPressed(GlobalAction action) diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs new file mode 100644 index 0000000000..0924773338 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -0,0 +1,64 @@ +// 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 JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Match +{ + public class DrawableMatchRoom : DrawableRoom + { + public Action OnEdit; + + [Resolved] + private IAPIProvider api { get; set; } + + private readonly IBindable host = new Bindable(); + private readonly bool allowEdit; + + [CanBeNull] + private Drawable editButton; + + public DrawableMatchRoom(Room room, bool allowEdit = true) + : base(room) + { + this.allowEdit = allowEdit; + + host.BindTo(room.Host); + } + + [BackgroundDependencyLoader] + private void load() + { + if (allowEdit) + { + ButtonsContainer.Add(editButton = new PurpleTriangleButton + { + RelativeSizeAxes = Axes.Y, + Size = new Vector2(100, 1), + Text = "Edit", + Action = () => OnEdit?.Invoke() + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (editButton != null) + host.BindValueChanged(h => editButton.Alpha = h.NewValue?.Equals(api.LocalUser.Value) == true ? 1 : 0, true); + } + + protected override Drawable CreateBackground() => new RoomBackgroundSprite(); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundSprite.cs new file mode 100644 index 0000000000..97262dd229 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundSprite.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps.Drawables; + +namespace osu.Game.Screens.OnlinePlay.Match +{ + public class RoomBackgroundSprite : RoomSubScreenComposite + { + protected readonly BeatmapSetCoverType BeatmapSetCoverType; + private UpdateableBeatmapBackgroundSprite sprite; + + public RoomBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover) + { + BeatmapSetCoverType = beatmapSetCoverType; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = sprite = new UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedItem.BindValueChanged(item => sprite.Beatmap.Value = item.NewValue?.Beatmap.Value, true); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 243d2abf74..10df7cbadc 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -31,8 +31,6 @@ namespace osu.Game.Screens.OnlinePlay.Match public override bool DisallowExternalBeatmapRulesetChanges => true; - private readonly ModSelectOverlay userModsSelectOverlay; - /// /// A container that provides controls for selection of user mods. /// This will be shown/hidden automatically when applicable. @@ -46,6 +44,8 @@ namespace osu.Game.Screens.OnlinePlay.Match /// protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); + protected readonly IBindable RoomId = new Bindable(); + [Resolved] private MusicController music { get; set; } @@ -58,55 +58,184 @@ namespace osu.Game.Screens.OnlinePlay.Match private IBindable> managerUpdated; [Cached] - protected OnlinePlayBeatmapAvailabilityTracker BeatmapAvailabilityTracker { get; } + protected OnlinePlayBeatmapAvailabilityTracker BeatmapAvailabilityTracker { get; private set; } protected IBindable BeatmapAvailability => BeatmapAvailabilityTracker.Availability; - protected RoomSubScreen() + public readonly Room Room; + private readonly bool allowEdit; + + private ModSelectOverlay userModsSelectOverlay; + private RoomSettingsOverlay settingsOverlay; + private Drawable mainContent; + + /// + /// Creates a new . + /// + /// The . + /// Whether to allow editing room settings post-creation. + protected RoomSubScreen(Room room, bool allowEdit = true) { + Room = room; + this.allowEdit = allowEdit; + Padding = new MarginPadding { Top = Header.HEIGHT }; - AddRangeInternal(new Drawable[] + BeatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"3e3a44") // This is super temporary. - }, - BeatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker - { - SelectedItem = { BindTarget = SelectedItem } - }, - new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Depth = float.MinValue, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }, - Child = userModsSelectOverlay = new UserModSelectOverlay - { - SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false - } - }, - }); - } + SelectedItem = { BindTarget = SelectedItem } + }; - protected override void ClearInternal(bool disposeChildren = true) => - throw new InvalidOperationException($"{nameof(RoomSubScreen)}'s children should not be cleared as it will remove required components"); + RoomId.BindTo(room.RoomID); + } [BackgroundDependencyLoader] private void load(AudioManager audio) { sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); + + InternalChildren = new Drawable[] + { + BeatmapAvailabilityTracker, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 50) + }, + Content = new[] + { + // Padded main content (drawable room + main content) + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Bottom = 30 + }, + Children = new[] + { + mainContent = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10) + }, + Content = new[] + { + new Drawable[] + { + new DrawableMatchRoom(Room, allowEdit) + { + OnEdit = () => settingsOverlay.Show() + } + }, + null, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(20), + Child = CreateMainContent(), + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = userModsSelectOverlay = new UserModSelectOverlay + { + SelectedMods = { BindTarget = UserMods }, + IsValidMod = _ => false + } + }, + } + } + } + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + // Resolves 1px masking errors between the settings overlay and the room panel. + Padding = new MarginPadding(-1), + Child = settingsOverlay = CreateRoomSettingsOverlay() + } + }, + }, + }, + // Footer + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(5), + Child = CreateFooter() + }, + } + } + } + } + } + }; } protected override void LoadComplete() { base.LoadComplete(); + RoomId.BindValueChanged(id => + { + if (id.NewValue == null) + { + // A new room is being created. + // The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed. + mainContent.Hide(); + settingsOverlay.Show(); + } + else + { + mainContent.Show(); + settingsOverlay.Hide(); + } + }, true); + SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); @@ -117,12 +246,25 @@ namespace osu.Game.Screens.OnlinePlay.Match public override bool OnBackButton() { + if (Room.RoomID.Value == null) + { + // room has not been created yet; exit immediately. + settingsOverlay.Hide(); + return base.OnBackButton(); + } + if (userModsSelectOverlay.State.Value == Visibility.Visible) { userModsSelectOverlay.Hide(); return true; } + if (settingsOverlay.State.Value == Visibility.Visible) + { + settingsOverlay.Hide(); + return true; + } + return base.OnBackButton(); } @@ -257,6 +399,21 @@ namespace osu.Game.Screens.OnlinePlay.Match track.Looping = false; } + /// + /// Creates the main centred content. + /// + protected abstract Drawable CreateMainContent(); + + /// + /// Creates the footer content. + /// + protected abstract Drawable CreateFooter(); + + /// + /// Creates the room settings overlay. + /// + protected abstract RoomSettingsOverlay CreateRoomSettingsOverlay(); + private class UserModSelectOverlay : LocalPlayerModSelectOverlay { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs new file mode 100644 index 0000000000..9fe41842f3 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -0,0 +1,114 @@ +// 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.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Input.Bindings; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.Play; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class GameplayChatDisplay : MatchChatDisplay, IKeyBindingHandler + { + [Resolved] + private ILocalUserPlayInfo localUserInfo { get; set; } + + private IBindable localUserPlaying = new Bindable(); + + public override bool PropagatePositionalInputSubTree => !localUserPlaying.Value; + + public Bindable Expanded = new Bindable(); + + private readonly Bindable expandedFromTextboxFocus = new Bindable(); + + private const float height = 100; + + public override bool PropagateNonPositionalInputSubTree => true; + + public GameplayChatDisplay() + : base(leaveChannelOnDispose: false) + { + RelativeSizeAxes = Axes.X; + + Background.Alpha = 0.2f; + + Textbox.FocusLost = () => expandedFromTextboxFocus.Value = false; + } + + protected override bool OnHover(HoverEvent e) => true; // use UI mouse cursor. + + protected override void LoadComplete() + { + base.LoadComplete(); + + localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy(); + localUserPlaying.BindValueChanged(playing => + { + // for now let's never hold focus. this avoid misdirected gameplay keys entering chat. + // note that this is done within this callback as it triggers an un-focus as well. + Textbox.HoldFocus = false; + + // only hold focus (after sending a message) during breaks + Textbox.ReleaseFocusOnCommit = playing.NewValue; + }, true); + + Expanded.BindValueChanged(_ => updateExpandedState(), true); + expandedFromTextboxFocus.BindValueChanged(focus => + { + if (focus.NewValue) + updateExpandedState(); + else + { + // on finishing typing a message there should be a brief delay before hiding. + using (BeginDelayedSequence(600)) + updateExpandedState(); + } + }, true); + } + + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.ToggleChatFocus: + if (Textbox.HasFocus) + { + Schedule(() => Textbox.KillFocus()); + } + else + { + expandedFromTextboxFocus.Value = true; + + // schedule required to ensure the textbox has become present from above bindable update. + Schedule(() => Textbox.TakeFocus()); + } + + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } + + private void updateExpandedState() + { + if (Expanded.Value || expandedFromTextboxFocus.Value) + { + this.FadeIn(300, Easing.OutQuint); + this.ResizeHeightTo(height, 500, Easing.OutQuint); + } + else + { + this.FadeOut(300, Easing.OutQuint); + this.ResizeHeightTo(0, 500, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index 56b87302c2..2e94e51385 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Online.API; using osu.Game.Screens.OnlinePlay.Match.Components; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { @@ -35,6 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, + Spacing = new Vector2(5), Children = new Drawable[] { beatmapPanelContainer = new Container diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index d4f5428bfb..036e37ddfd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -2,18 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerMatchFooter : CompositeDrawable { - public const float HEIGHT = 50; private const float ready_button_width = 600; private const float spectate_button_width = 200; @@ -27,54 +22,42 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match set => spectateButton.OnSpectateClick = value; } - private readonly Drawable background; private readonly MultiplayerReadyButton readyButton; private readonly MultiplayerSpectateButton spectateButton; public MultiplayerMatchFooter() { - RelativeSizeAxes = Axes.X; - Height = HEIGHT; + RelativeSizeAxes = Axes.Both; - InternalChildren = new[] + InternalChild = new GridContainer { - background = new Box { RelativeSizeAxes = Axes.Both }, - new GridContainer + RelativeSizeAxes = Axes.Both, + Content = new[] { - RelativeSizeAxes = Axes.Both, - Content = new[] + new Drawable[] { - new Drawable[] + null, + spectateButton = new MultiplayerSpectateButton { - null, - spectateButton = new MultiplayerSpectateButton - { - RelativeSizeAxes = Axes.Both, - }, - null, - readyButton = new MultiplayerReadyButton - { - RelativeSizeAxes = Axes.Both, - }, - null - } - }, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(maxSize: spectate_button_width), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(maxSize: ready_button_width), - new Dimension() + RelativeSizeAxes = Axes.Both, + }, + null, + readyButton = new MultiplayerReadyButton + { + RelativeSizeAxes = Axes.Both, + }, + null } + }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(maxSize: spectate_button_width), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(maxSize: ready_button_width), + new Dimension() } }; } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - background.Colour = Color4Extensions.FromHex(@"28242d"); - } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs deleted file mode 100644 index bb351d06d3..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; -using osu.Game.Screens.OnlinePlay.Match.Components; -using osu.Game.Users.Drawables; -using osuTK; -using FontWeight = osu.Game.Graphics.FontWeight; -using OsuColour = osu.Game.Graphics.OsuColour; -using OsuFont = osu.Game.Graphics.OsuFont; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match -{ - public class MultiplayerMatchHeader : OnlinePlayComposite - { - public const float HEIGHT = 50; - - public Action OpenSettings; - - private UpdateableAvatar avatar; - private LinkFlowContainer hostText; - private Button openSettingsButton; - - [Resolved] - private IAPIProvider api { get; set; } - - public MultiplayerMatchHeader() - { - RelativeSizeAxes = Axes.X; - Height = HEIGHT; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - InternalChildren = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - avatar = new UpdateableAvatar - { - Size = new Vector2(50), - Masking = true, - CornerRadius = 10, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(size: 30), - Current = { BindTarget = RoomName } - }, - hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 20)) - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - } - } - } - } - }, - openSettingsButton = new PurpleTriangleButton - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(150, HEIGHT), - Text = "Open settings", - Action = () => OpenSettings?.Invoke(), - Alpha = 0 - } - }; - - Host.BindValueChanged(host => - { - avatar.User = host.NewValue; - - hostText.Clear(); - - if (host.NewValue != null) - { - hostText.AddText("hosted by "); - hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); - } - - openSettingsButton.Alpha = host.NewValue?.Equals(api.LocalUser.Value) == true ? 1 : 0; - }, true); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 5f3921d742..930b92c528 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -26,7 +26,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class MultiplayerMatchSettingsOverlay : MatchSettingsOverlay + public class MultiplayerMatchSettingsOverlay : RoomSettingsOverlay { private MatchSettings settings; @@ -150,6 +150,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, + LengthLimit = 100, }, }, new Section("Room visibility") @@ -207,6 +208,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, + LengthLimit = 255, }, }, } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 45928505bb..58b5b7bbeb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay.Components; @@ -23,35 +22,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.ChangeState(MultiplayerUserState.Idle); } - protected override void UpdatePollingRate(bool isIdle) - { - var multiplayerRoomManager = (MultiplayerRoomManager)RoomManager; - - if (!this.IsCurrentScreen()) - { - multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; - multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; - } - else - { - switch (CurrentSubScreen) - { - case LoungeSubScreen _: - multiplayerRoomManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; - multiplayerRoomManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; - break; - - // Don't poll inside the match or anywhere else. - default: - multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; - multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; - break; - } - } - - Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value}, selection: {multiplayerRoomManager.TimeBetweenSelectionPolls.Value})"); - } - protected override string ScreenTitle => "Multiplayer"; protected override RoomManager CreateRoomManager() => new MultiplayerRoomManager(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index ad7882abc2..6c3dfe7382 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -1,12 +1,16 @@ // 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.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Logging; +using osu.Framework.Screens; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -21,6 +25,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } + public override void OnResuming(IScreen last) + { + base.OnResuming(last); + + // Upon having left a room, we don't know whether we were the only participant, and whether the room is now closed as a result of leaving it. + // To work around this, temporarily remove the room and trigger an immediate listing poll. + if (last is MultiplayerMatchSubScreen match) + { + RoomManager.RemoveRoom(match.Room); + ListingPollingComponent.PollImmediately(); + } + } + protected override FilterCriteria CreateFilterCriteria() { var criteria = base.CreateFilterCriteria(); @@ -33,12 +50,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override Room CreateNewRoom() => new Room { Name = { Value = $"{api.LocalUser}'s awesome room" }, - Category = { Value = RoomCategory.Realtime }, Type = { Value = MatchType.HeadToHead }, }; protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); + protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); + protected override void OpenNewRoom(Room room) { if (client?.IsConnected.Value != true) @@ -49,5 +67,32 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.OpenNewRoom(room); } + + private class MultiplayerListingPollingComponent : ListingPollingComponent + { + [Resolved] + private MultiplayerClient client { get; set; } + + private readonly IBindable isConnected = new Bindable(); + + [BackgroundDependencyLoader] + private void load() + { + isConnected.BindTo(client.IsConnected); + isConnected.BindValueChanged(c => Scheduler.AddOnce(() => + { + if (isConnected.Value && IsLoaded) + PollImmediately(); + }), true); + } + + protected override Task Poll() + { + if (!isConnected.Value) + return Task.CompletedTask; + + return base.Poll(); + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 1943ff668f..3a012787a4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -49,215 +49,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private OngoingOperationTracker ongoingOperationTracker { get; set; } [Resolved] - private Bindable currentRoom { get; set; } - - private MultiplayerMatchSettingsOverlay settingsOverlay; + private Bindable currentRoom { get; set; } // Todo: This should not exist. private readonly IBindable isConnected = new Bindable(); [CanBeNull] private IDisposable readyClickOperation; - private GridContainer mainContent; - public MultiplayerMatchSubScreen(Room room) + : base(room) { Title = room.RoomID.Value == null ? "New room" : room.Name.Value; Activity.Value = new UserActivity.InLobby(room); } - [BackgroundDependencyLoader] - private void load() - { - AddRangeInternal(new Drawable[] - { - mainContent = new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Horizontal = HORIZONTAL_OVERFLOW_PADDING + 55, - Vertical = 20 - }, - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - }, - Content = new[] - { - new Drawable[] - { - new MultiplayerMatchHeader - { - OpenSettings = () => settingsOverlay.Show() - } - }, - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 400), - new Dimension(), - new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 600), - }, - Content = new[] - { - new Drawable[] - { - // Main left column - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] { new ParticipantsListHeader() }, - new Drawable[] - { - new ParticipantsList - { - RelativeSizeAxes = Axes.Both - }, - } - } - }, - // Spacer - null, - // Main right column - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new OverlinedHeader("Beatmap"), - new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } - } - }, - UserModsSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 10 }, - Children = new Drawable[] - { - new OverlinedHeader("Extra mods"), - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, - } - } - } - } - } - } - } - } - } - } - }, - new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] { new OverlinedHeader("Chat") }, - new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } } - } - } - } - }, - } - } - }, - new Drawable[] - { - new MultiplayerMatchFooter - { - OnReadyClick = onReadyClick, - OnSpectateClick = onSpectateClick - } - } - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - } - }, - settingsOverlay = new MultiplayerMatchSettingsOverlay - { - RelativeSizeAxes = Axes.Both, - State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden } - } - }); - - if (client.Room == null) - { - // A new room is being created. - // The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed. - mainContent.Hide(); - - settingsOverlay.State.BindValueChanged(visibility => - { - if (visibility.NewValue == Visibility.Hidden) - mainContent.Show(); - }, true); - } - } - protected override void LoadComplete() { base.LoadComplete(); @@ -289,6 +94,126 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, true); } + protected override Drawable CreateMainContent() => new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 400), + new Dimension(), + new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 600), + }, + Content = new[] + { + new Drawable[] + { + // Main left column + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] { new ParticipantsListHeader() }, + new Drawable[] + { + new ParticipantsList + { + RelativeSizeAxes = Axes.Both + }, + } + } + }, + // Spacer + null, + // Main right column + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { new OverlinedHeader("Beatmap") }, + new Drawable[] { new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } }, + new[] + { + UserModsSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Extra mods"), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, + } + }, + } + }, + }, + new Drawable[] { new OverlinedHeader("Chat") { Margin = new MarginPadding { Vertical = 5 }, }, }, + new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } } + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + } + }, + } + } + } + } + }, + }, + }; + + protected override Drawable CreateFooter() => new MultiplayerMatchFooter + { + OnReadyClick = onReadyClick, + OnSpectateClick = onSpectateClick + }; + + protected override RoomSettingsOverlay CreateRoomSettingsOverlay() => new MultiplayerMatchSettingsOverlay(); + protected override void UpdateMods() { if (SelectedItem.Value == null || client.LocalUser == null) @@ -305,23 +230,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private bool exitConfirmed; - public override bool OnBackButton() - { - if (client.Room == null) - { - // room has not been created yet; exit immediately. - return base.OnBackButton(); - } - - if (settingsOverlay.State.Value == Visibility.Visible) - { - settingsOverlay.Hide(); - return true; - } - - return base.OnBackButton(); - } - public override bool OnExiting(IScreen next) { // the room may not be left immediately after a disconnection due to async flow, @@ -505,19 +413,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (client != null) - { - client.RoomUpdated -= onRoomUpdated; - client.LoadRequested -= onLoadRequested; - } - - modSettingChangeTracker?.Dispose(); - } - public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) { if (!this.IsCurrentScreen()) @@ -533,5 +428,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer this.Push(new MultiplayerMatchSongSelect(beatmap, ruleset)); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client != null) + { + client.RoomUpdated -= onRoomUpdated; + client.LoadRequested -= onLoadRequested; + } + + modSettingChangeTracker?.Dispose(); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index ca1a3710ab..24657943d7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -68,6 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, + Spacing = new Vector2(5) }); // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. @@ -78,7 +79,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer ((IBindable)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud); - leaderboardFlow.Add(l); + leaderboardFlow.Insert(0, l); if (leaderboard.TeamScores.Count >= 2) { @@ -87,10 +88,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Team1Score = { BindTarget = leaderboard.TeamScores.First().Value }, Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value }, Expanded = { BindTarget = HUDOverlay.ShowHud }, - }, leaderboardFlow.Add); + }, scoreDisplay => leaderboardFlow.Insert(1, scoreDisplay)); } }); + LoadComponentAsync(new GameplayChatDisplay + { + Expanded = { BindTarget = HUDOverlay.ShowHud }, + }, chat => leaderboardFlow.Insert(2, chat)); + HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue }); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs index cbba4babe5..2d94b2328d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -2,11 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; using osu.Game.Online.Multiplayer; @@ -21,22 +18,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient multiplayerClient { get; set; } - public readonly Bindable TimeBetweenListingPolls = new Bindable(); - public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); - private readonly IBindable isConnected = new Bindable(); - private readonly Bindable allowPolling = new Bindable(); - - private ListingPollingComponent listingPollingComponent; - - protected override void LoadComplete() - { - base.LoadComplete(); - - isConnected.BindTo(multiplayerClient.IsConnected); - isConnected.BindValueChanged(_ => Scheduler.AddOnce(updatePolling)); - JoinedRoom.BindValueChanged(_ => Scheduler.AddOnce(updatePolling), true); - } - public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => base.CreateRoom(room, r => joinMultiplayerRoom(r, r.Password.Value, onSuccess, onError), onError); @@ -64,19 +45,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (JoinedRoom.Value == null) return; - var joinedRoom = JoinedRoom.Value; - base.PartRoom(); - multiplayerClient.LeaveRoom(); - - // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. - // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. - Schedule(() => - { - RemoveRoom(joinedRoom); - listingPollingComponent.PollImmediately(); - }); } private void joinMultiplayerRoom(Room room, string password, Action onSuccess = null, Action onError = null) @@ -99,70 +69,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } }); } - - private void updatePolling() - { - if (!isConnected.Value) - ClearRooms(); - - // Don't poll when not connected or when a room has been joined. - allowPolling.Value = isConnected.Value && JoinedRoom.Value == null; - } - - protected override IEnumerable CreatePollingComponents() => new RoomPollingComponent[] - { - listingPollingComponent = new MultiplayerListingPollingComponent - { - TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, - AllowPolling = { BindTarget = allowPolling } - }, - new MultiplayerSelectionPollingComponent - { - TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls }, - AllowPolling = { BindTarget = allowPolling } - } - }; - - private class MultiplayerListingPollingComponent : ListingPollingComponent - { - public readonly IBindable AllowPolling = new Bindable(); - - protected override void LoadComplete() - { - base.LoadComplete(); - - AllowPolling.BindValueChanged(allowPolling => - { - if (!allowPolling.NewValue) - return; - - if (IsLoaded) - PollImmediately(); - }); - } - - protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); - } - - private class MultiplayerSelectionPollingComponent : SelectionPollingComponent - { - public readonly IBindable AllowPolling = new Bindable(); - - protected override void LoadComplete() - { - base.LoadComplete(); - - AllowPolling.BindValueChanged(allowPolling => - { - if (!allowPolling.NewValue) - return; - - if (IsLoaded) - PollImmediately(); - }); - } - - protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); - } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 2c157b0564..ececa1e497 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// The score containing the player's replay. /// The clock controlling the gameplay running state. public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock) - : base(score) + : base(score, new PlayerConfiguration { AllowUserInteraction = false }) { this.spectatorPlayerClock = spectatorPlayerClock; } @@ -34,6 +34,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void load() { spectatorPlayerClock.WaitingOnFrames.BindTo(waitingOnFrames); + + HUDOverlay.PlayerSettingsOverlay.Expire(); + HUDOverlay.HoldToQuit.Expire(); } protected override void UpdateAfterChildren() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs index 5a1d28e9c4..14bd8fa6dc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs @@ -3,6 +3,7 @@ using System; using JetBrains.Annotations; +using osu.Framework.Allocation; using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -19,6 +20,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } + [BackgroundDependencyLoader] + private void load() + { + PlayerSettings.Expire(); + } + protected override void LogoArriving(OsuLogo logo, bool resuming) { } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index fd265e9978..e5962db608 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -14,14 +14,12 @@ using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Containers; -using osu.Game.Input; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; -using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Users; using osuTK; using osuTK.Graphics; @@ -44,17 +42,12 @@ namespace osu.Game.Screens.OnlinePlay private LoungeSubScreen loungeSubScreen; private ScreenStack screenStack; - private readonly IBindable isIdle = new BindableBool(); - [Cached(Type = typeof(IRoomManager))] protected RoomManager RoomManager { get; private set; } [Cached] private readonly Bindable selectedRoom = new Bindable(); - [Cached] - private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); - [Cached] private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); @@ -67,9 +60,6 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] protected IAPIProvider API { get; private set; } - [Resolved(CanBeNull = true)] - private IdleTracker idleTracker { get; set; } - [Resolved(CanBeNull = true)] private OsuLogo logo { get; set; } @@ -147,12 +137,6 @@ namespace osu.Game.Screens.OnlinePlay apiState.BindTo(API.State); apiState.BindValueChanged(onlineStateChanged, true); - - if (idleTracker != null) - { - isIdle.BindTo(idleTracker.IsIdle); - isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true); - } } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -162,8 +146,6 @@ namespace osu.Game.Screens.OnlinePlay return dependencies; } - protected abstract void UpdatePollingRate(bool isIdle); - private void forcefullyExit() { // This is temporary since we don't currently have a way to force screens to be exited @@ -199,8 +181,6 @@ namespace osu.Game.Screens.OnlinePlay screenStack.CurrentScreen.OnResuming(last); base.OnResuming(last); - - UpdatePollingRate(isIdle.Value); } public override void OnSuspending(IScreen next) @@ -210,8 +190,6 @@ namespace osu.Game.Screens.OnlinePlay Debug.Assert(screenStack.CurrentScreen != null); screenStack.CurrentScreen.OnSuspending(next); - - UpdatePollingRate(isIdle.Value); } public override bool OnExiting(IScreen next) @@ -275,15 +253,13 @@ namespace osu.Game.Screens.OnlinePlay if (newScreen is IOsuScreen newOsuScreen) ((IBindable)Activity).BindTo(newOsuScreen.Activity); - - UpdatePollingRate(isIdle.Value); } protected IScreen CurrentSubScreen => screenStack.CurrentScreen; protected abstract string ScreenTitle { get; } - protected abstract RoomManager CreateRoomManager(); + protected virtual RoomManager CreateRoomManager() => new RoomManager(); protected abstract LoungeSubScreen CreateLounge(); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index e1bd889088..49cc77a5d8 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -23,12 +23,6 @@ namespace osu.Game.Screens.OnlinePlay RelativeSizeAxes = Axes.Both; } - public const float X_SHIFT = 200; - - public const double X_MOVE_DURATION = 800; - - public const double RESUME_TRANSITION_DELAY = DISAPPEAR_DURATION / 2; - public const double APPEAR_DURATION = 800; public const double DISAPPEAR_DURATION = 500; @@ -36,28 +30,23 @@ namespace osu.Game.Screens.OnlinePlay public override void OnEntering(IScreen last) { this.FadeInFromZero(APPEAR_DURATION, Easing.OutQuint); - this.FadeInFromZero(APPEAR_DURATION, Easing.OutQuint); - this.MoveToX(X_SHIFT).MoveToX(0, X_MOVE_DURATION, Easing.OutQuint); } public override bool OnExiting(IScreen next) { this.FadeOut(DISAPPEAR_DURATION, Easing.OutQuint); - this.MoveToX(X_SHIFT, X_MOVE_DURATION, Easing.OutQuint); return false; } public override void OnResuming(IScreen last) { - this.Delay(RESUME_TRANSITION_DELAY).FadeIn(APPEAR_DURATION, Easing.OutQuint); - this.MoveToX(0, X_MOVE_DURATION, Easing.OutQuint); + this.FadeIn(APPEAR_DURATION, Easing.OutQuint); } public override void OnSuspending(IScreen next) { this.FadeOut(DISAPPEAR_DURATION, Easing.OutQuint); - this.MoveToX(-X_SHIFT, X_MOVE_DURATION, Easing.OutQuint); } public override string ToString() => Title; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs index 6a78e24ba1..1edeef77df 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs @@ -1,53 +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 osu.Framework.Logging; -using osu.Framework.Screens; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; -using osu.Game.Screens.OnlinePlay.Match; namespace osu.Game.Screens.OnlinePlay.Playlists { public class Playlists : OnlinePlayScreen { - protected override void UpdatePollingRate(bool isIdle) - { - var playlistsManager = (PlaylistsRoomManager)RoomManager; - - if (!this.IsCurrentScreen()) - { - playlistsManager.TimeBetweenListingPolls.Value = 0; - playlistsManager.TimeBetweenSelectionPolls.Value = 0; - } - else - { - switch (CurrentSubScreen) - { - case LoungeSubScreen _: - playlistsManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; - playlistsManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; - break; - - case RoomSubScreen _: - playlistsManager.TimeBetweenListingPolls.Value = 0; - playlistsManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000; - break; - - default: - playlistsManager.TimeBetweenListingPolls.Value = 0; - playlistsManager.TimeBetweenSelectionPolls.Value = 0; - break; - } - } - - Logger.Log($"Polling adjusted (listing: {playlistsManager.TimeBetweenListingPolls.Value}, selection: {playlistsManager.TimeBetweenSelectionPolls.Value})"); - } - protected override string ScreenTitle => "Playlists"; - protected override RoomManager CreateRoomManager() => new PlaylistsRoomManager(); - protected override LoungeSubScreen CreateLounge() => new PlaylistsLoungeSubScreen(); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index eee4d4f407..dced9b8691 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -66,6 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); + protected override ListingPollingComponent CreatePollingComponent() => new ListingPollingComponent(); + private enum PlaylistsCategory { Any, diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs new file mode 100644 index 0000000000..3eb1cde0a4 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs @@ -0,0 +1,32 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public class PlaylistsRoomFooter : CompositeDrawable + { + public Action OnStart; + + public PlaylistsRoomFooter() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new[] + { + new PlaylistsReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(600, 1), + Action = () => OnStart?.Invoke() + } + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs deleted file mode 100644 index c55d1c3e94..0000000000 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs +++ /dev/null @@ -1,21 +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.Collections.Generic; -using osu.Framework.Bindables; -using osu.Game.Screens.OnlinePlay.Components; - -namespace osu.Game.Screens.OnlinePlay.Playlists -{ - public class PlaylistsRoomManager : RoomManager - { - public readonly Bindable TimeBetweenListingPolls = new Bindable(); - public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); - - protected override IEnumerable CreatePollingComponents() => new RoomPollingComponent[] - { - new ListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls } }, - new SelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls } } - }; - } -} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs similarity index 99% rename from osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 2640f99ea5..3eea59006a 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -22,7 +22,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Playlists { - public class PlaylistsMatchSettingsOverlay : MatchSettingsOverlay + public class PlaylistsRoomSettingsOverlay : RoomSettingsOverlay { public Action EditPlaylist; @@ -193,7 +193,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Child = new GridContainer { RelativeSizeAxes = Axes.X, - Height = 500, + Height = 448, Content = new[] { new Drawable[] diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 953c687087..729ddbef8c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -3,11 +3,14 @@ using System.Diagnostics; 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.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Input; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; @@ -17,7 +20,6 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osuTK; -using Footer = osu.Game.Screens.OnlinePlay.Match.Components.Footer; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -27,254 +29,176 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public override string ShortTitle => "playlist"; - [Resolved(typeof(Room), nameof(Room.RoomID))] - private Bindable roomId { get; set; } + [Resolved] + private IAPIProvider api { get; set; } - [Resolved(typeof(Room), nameof(Room.Playlist))] - private BindableList playlist { get; set; } + private readonly IBindable isIdle = new BindableBool(); - private MatchSettingsOverlay settingsOverlay; private MatchLeaderboard leaderboard; - - private OverlinedHeader participantsHeader; - - private GridContainer mainContent; + private SelectionPollingComponent selectionPollingComponent; public PlaylistsRoomSubScreen(Room room) + : base(room, false) // Editing is temporarily not allowed. { Title = room.RoomID.Value == null ? "New playlist" : room.Name.Value; Activity.Value = new UserActivity.InLobby(room); } - [BackgroundDependencyLoader] - private void load() + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] IdleTracker idleTracker) { - AddRangeInternal(new Drawable[] - { - mainContent = new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Horizontal = 105, - Vertical = 20 - }, - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - }, - Content = new[] - { - new Drawable[] { new Match.Components.Header() }, - new Drawable[] - { - participantsHeader = new OverlinedHeader("Participants") - { - ShowLine = false - } - }, - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 5 }, - Child = new ParticipantsDisplay(Direction.Horizontal) - { - Details = { BindTarget = participantsHeader.Details } - } - } - }, - new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 5 }, - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] { new OverlinedPlaylistHeader(), }, - new Drawable[] - { - new DrawableRoomPlaylistWithResults - { - RelativeSizeAxes = Axes.Both, - Items = { BindTarget = playlist }, - SelectedItem = { BindTarget = SelectedItem }, - RequestShowResults = item => - { - Debug.Assert(roomId.Value != null); - ParentScreen?.Push(new PlaylistsResultsScreen(null, roomId.Value.Value, item, false)); - } - } - }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new[] - { - UserModsSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 10 }, - Children = new Drawable[] - { - new OverlinedHeader("Extra mods"), - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, - } - } - } - }, - }, - new Drawable[] - { - new OverlinedHeader("Leaderboard") - }, - new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, }, - new Drawable[] { new OverlinedHeader("Chat"), }, - new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } } - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 120), - } - }, - null - }, - }, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 400), - new Dimension(), - new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 600), - new Dimension(), - } - } - } - }, - } - } - }, - new Drawable[] - { - new Footer { OnStart = StartPlay } - } - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - } - }, - settingsOverlay = new PlaylistsMatchSettingsOverlay - { - RelativeSizeAxes = Axes.Both, - EditPlaylist = () => - { - if (this.IsCurrentScreen()) - this.Push(new PlaylistsSongSelect()); - }, - State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden } - } - }); + if (idleTracker != null) + isIdle.BindTo(idleTracker.IsIdle); - if (roomId.Value == null) - { - // A new room is being created. - // The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed. - mainContent.Hide(); - - settingsOverlay.State.BindValueChanged(visibility => - { - if (visibility.NewValue == Visibility.Hidden) - mainContent.Show(); - }, true); - } + AddInternal(selectionPollingComponent = new SelectionPollingComponent()); } - [Resolved] - private IAPIProvider api { get; set; } - protected override void LoadComplete() { base.LoadComplete(); - roomId.BindValueChanged(id => + isIdle.BindValueChanged(_ => updatePollingRate(), true); + RoomId.BindValueChanged(id => { - if (id.NewValue == null) - settingsOverlay.Show(); - else + if (id.NewValue != null) { - settingsOverlay.Hide(); - // Set the first playlist item. // This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()). - Schedule(() => SelectedItem.Value = playlist.FirstOrDefault()); + Schedule(() => SelectedItem.Value = Room.Playlist.FirstOrDefault()); } }, true); } + protected override Drawable CreateMainContent() => new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 5 }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { new OverlinedPlaylistHeader(), }, + new Drawable[] + { + new DrawableRoomPlaylistWithResults + { + RelativeSizeAxes = Axes.Both, + Items = { BindTarget = Room.Playlist }, + SelectedItem = { BindTarget = SelectedItem }, + RequestShowResults = item => + { + Debug.Assert(RoomId.Value != null); + ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item, false)); + } + } + }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + } + } + }, + null, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new[] + { + UserModsSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Margin = new MarginPadding { Bottom = 10 }, + Children = new Drawable[] + { + new OverlinedHeader("Extra mods"), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, + } + } + } + }, + }, + new Drawable[] + { + new OverlinedHeader("Leaderboard") + }, + new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, }, + new Drawable[] { new OverlinedHeader("Chat"), }, + new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } } + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 120), + } + }, + }, + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 400), + new Dimension(), + new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 600), + } + }; + + protected override Drawable CreateFooter() => new PlaylistsRoomFooter + { + OnStart = StartPlay + }; + + protected override RoomSettingsOverlay CreateRoomSettingsOverlay() => new PlaylistsRoomSettingsOverlay + { + EditPlaylist = () => + { + if (this.IsCurrentScreen()) + this.Push(new PlaylistsSongSelect()); + }, + }; + + private void updatePollingRate() + { + selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; + Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})"); + } + protected override Screen CreateGameplayScreen() => new PlayerLoader(() => new PlaylistsPlayer(SelectedItem.Value) { Exited = () => leaderboard.RefreshScores() diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index 4265a83ce1..d77673580a 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -10,12 +10,12 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; -using osu.Game.Screens.Ranking.Expanded; using osuTK; namespace osu.Game.Screens.Play diff --git a/osu.Game/Screens/Play/ILocalUserPlayInfo.cs b/osu.Game/Screens/Play/ILocalUserPlayInfo.cs new file mode 100644 index 0000000000..9a2259b12f --- /dev/null +++ b/osu.Game/Screens/Play/ILocalUserPlayInfo.cs @@ -0,0 +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.Bindables; + +namespace osu.Game.Screens.Play +{ + [Cached] + public interface ILocalUserPlayInfo + { + /// + /// Whether the local user is currently playing. + /// + IBindable IsPlaying { get; } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 09eaf1c543..59c7abb299 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play { [Cached] [Cached(typeof(ISamplePlaybackDisabler))] - public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler + public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler, ILocalUserPlayInfo { /// /// The delay upon completion of the beatmap before displaying the results screen. @@ -75,7 +75,9 @@ namespace osu.Game.Screens.Play private readonly Bindable storyboardReplacesBackground = new Bindable(); - protected readonly Bindable LocalUserPlaying = new Bindable(); + public IBindable LocalUserPlaying => localUserPlaying; + + private readonly Bindable localUserPlaying = new Bindable(); public int RestartCount; @@ -442,7 +444,7 @@ namespace osu.Game.Screens.Play { bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value; OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; - LocalUserPlaying.Value = inGameplay; + localUserPlaying.Value = inGameplay; } private void updateSampleDisabledState() @@ -1052,5 +1054,7 @@ namespace osu.Game.Screens.Play #endregion IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; + + IBindable ILocalUserPlayInfo.IsPlaying => LocalUserPlaying; } } diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs index 18ee73374f..3aa424e5d5 100644 --- a/osu.Game/Screens/Play/PlayerConfiguration.cs +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -20,6 +20,11 @@ namespace osu.Game.Screens.Play /// public bool AllowRestart { get; set; } = true; + /// + /// Whether the player should be able to interact with this player instance. + /// + public bool AllowUserInteraction { get; set; } = true; + /// /// Whether the player should be allowed to skip intros/outros, advancing to the start of gameplay or the end of a storyboard. /// diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 5f6b4ca2b0..969527a758 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -46,9 +46,14 @@ namespace osu.Game.Screens.Play protected override bool PlayResumeSound => false; - protected BeatmapMetadataDisplay MetadataInfo; + protected BeatmapMetadataDisplay MetadataInfo { get; private set; } - protected VisualSettings VisualSettings; + /// + /// A fill flow containing the player settings groups, exposed for the ability to hide it from inheritors of the player loader. + /// + protected FillFlowContainer PlayerSettings { get; private set; } + + protected VisualSettings VisualSettings { get; private set; } protected Task LoadTask { get; private set; } @@ -140,7 +145,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - new FillFlowContainer + PlayerSettings = new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index 6aa7e017ce..b27a9c5f5d 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -119,13 +119,14 @@ namespace osu.Game.Screens.Play if (drawableRuleset != null) { - AllowSeeking.BindTo(drawableRuleset.HasReplayLoaded); + if (player?.Configuration.AllowUserInteraction == true) + ((IBindable)AllowSeeking).BindTo(drawableRuleset.HasReplayLoaded); referenceClock = drawableRuleset.FrameStableClock; Objects = drawableRuleset.Objects; } - config.BindWith(OsuSetting.ShowDifficultyGraph, ShowGraph); + config.BindWith(OsuSetting.ShowProgressGraph, ShowGraph); graph.FillColour = bar.FillColour = colours.BlueLighter; } diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index f662a479ec..1dae28092a 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -23,7 +23,8 @@ namespace osu.Game.Screens.Play protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap - public SpectatorPlayer(Score score) + public SpectatorPlayer(Score score, PlayerConfiguration configuration = null) + : base(configuration) { this.score = score; } diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index e10fe5726d..bcb5e7999f 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; diff --git a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs deleted file mode 100644 index 2b86100be8..0000000000 --- a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs +++ /dev/null @@ -1,129 +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.Globalization; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Ranking.Expanded -{ - /// - /// A pill that displays the star rating of a . - /// - public class StarRatingDisplay : CompositeDrawable, IHasCurrentValue - { - private Box background; - private FillFlowContainer content; - private OsuTextFlowContainer textFlow; - - [Resolved] - private OsuColour colours { get; set; } - - private readonly BindableWithCurrent current = new BindableWithCurrent(); - - public Bindable Current - { - get => current.Current; - set => current.Current = value; - } - - /// - /// Creates a new using an already computed . - /// - /// The already computed to display the star difficulty of. - public StarRatingDisplay(StarDifficulty starDifficulty) - { - Current.Value = starDifficulty; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache) - { - AutoSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - }, - } - }, - content = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 8, Vertical = 4 }, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(2, 0), - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(7), - Icon = FontAwesome.Solid.Star, - }, - textFlow = new OsuTextFlowContainer(s => s.Font = OsuFont.Numeric.With(weight: FontWeight.Black)) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - TextAnchor = Anchor.BottomLeft, - } - } - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Current.BindValueChanged(_ => updateDisplay(), true); - } - - private void updateDisplay() - { - var starRatingParts = Current.Value.Stars.ToString("0.00", CultureInfo.InvariantCulture).Split('.'); - string wholePart = starRatingParts[0]; - string fractionPart = starRatingParts[1]; - string separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; - - var stars = Current.Value.Stars; - - background.Colour = colours.ForStarDifficulty(stars); - content.Colour = stars >= 6.5 ? colours.Orange1 : Color4.Black; - - textFlow.Clear(); - textFlow.AddText($"{wholePart}", s => - { - s.Font = s.Font.With(size: 14); - s.UseFullGlyphHeight = false; - }); - - textFlow.AddText($"{separator}{fractionPart}", s => - { - s.Font = s.Font.With(size: 7); - s.UseFullGlyphHeight = false; - }); - } - } -} diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 5b4e077100..297a16dd1d 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -28,7 +28,6 @@ using osu.Game.Extensions; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Ranking.Expanded; using osu.Game.Graphics.Containers; namespace osu.Game.Screens.Select @@ -38,6 +37,8 @@ namespace osu.Game.Screens.Select public const float BORDER_THICKNESS = 2.5f; private const float shear_width = 36.75f; + private const float transition_duration = 250; + private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0); [Resolved] @@ -46,11 +47,6 @@ namespace osu.Game.Screens.Select [Resolved] private IBindable> mods { get; set; } - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } - - private IBindable beatmapDifficulty; - protected Container DisplayedContent { get; private set; } protected WedgeInfoText Info { get; private set; } @@ -77,24 +73,24 @@ namespace osu.Game.Screens.Select ruleset.BindValueChanged(_ => updateDisplay()); } + private const double animation_duration = 800; + protected override void PopIn() { - this.MoveToX(0, 800, Easing.OutQuint); - this.RotateTo(0, 800, Easing.OutQuint); - this.FadeIn(250); + this.MoveToX(0, animation_duration, Easing.OutQuint); + this.RotateTo(0, animation_duration, Easing.OutQuint); + this.FadeIn(transition_duration); } protected override void PopOut() { - this.MoveToX(-100, 800, Easing.In); - this.RotateTo(10, 800, Easing.In); - this.FadeOut(500, Easing.In); + this.MoveToX(-100, animation_duration, Easing.In); + this.RotateTo(10, animation_duration, Easing.In); + this.FadeOut(transition_duration * 2, Easing.In); } private WorkingBeatmap beatmap; - private CancellationTokenSource cancellationSource; - public WorkingBeatmap Beatmap { get => beatmap; @@ -103,12 +99,6 @@ namespace osu.Game.Screens.Select if (beatmap == value) return; beatmap = value; - cancellationSource?.Cancel(); - cancellationSource = new CancellationTokenSource(); - - beatmapDifficulty?.UnbindAll(); - beatmapDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, cancellationSource.Token); - beatmapDifficulty.BindValueChanged(_ => updateDisplay()); updateDisplay(); } @@ -128,7 +118,7 @@ namespace osu.Game.Screens.Select { State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; - DisplayedContent?.FadeOut(250); + DisplayedContent?.FadeOut(transition_duration); DisplayedContent?.Expire(); DisplayedContent = null; } @@ -147,7 +137,7 @@ namespace osu.Game.Screens.Select Children = new Drawable[] { new BeatmapInfoWedgeBackground(beatmap), - Info = new WedgeInfoText(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value ?? new StarDifficulty()), + Info = new WedgeInfoText(beatmap, ruleset.Value, mods.Value), } }, loaded => { @@ -160,12 +150,6 @@ namespace osu.Game.Screens.Select } } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - cancellationSource?.Cancel(); - } - public class WedgeInfoText : Container { public OsuSpriteText VersionLabel { get; private set; } @@ -174,6 +158,9 @@ namespace osu.Game.Screens.Select public BeatmapSetOnlineStatusPill StatusPill { get; private set; } public FillFlowContainer MapperContainer { get; private set; } + private Container difficultyColourBar; + private StarRatingDisplay starRatingDisplay; + private ILocalisedBindableString titleBinding; private ILocalisedBindableString artistBinding; private FillFlowContainer infoLabelContainer; @@ -182,20 +169,21 @@ namespace osu.Game.Screens.Select private readonly WorkingBeatmap beatmap; private readonly RulesetInfo ruleset; private readonly IReadOnlyList mods; - private readonly StarDifficulty starDifficulty; private ModSettingChangeTracker settingChangeTracker; - public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList mods, StarDifficulty difficulty) + public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList mods) { this.beatmap = beatmap; ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset; this.mods = mods; - starDifficulty = difficulty; } + private CancellationTokenSource cancellationSource; + private IBindable starDifficulty; + [BackgroundDependencyLoader] - private void load(LocalisationManager localisation) + private void load(OsuColour colours, LocalisationManager localisation, BeatmapDifficultyCache difficultyCache) { var beatmapInfo = beatmap.BeatmapInfo; var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); @@ -205,12 +193,30 @@ namespace osu.Game.Screens.Select titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title)); artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist)); + const float top_height = 0.7f; + Children = new Drawable[] { - new DifficultyColourBar(starDifficulty) + difficultyColourBar = new Container { RelativeSizeAxes = Axes.Y, - Width = 20, + Width = 20f, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Width = top_height, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Alpha = 0.5f, + X = top_height, + Width = 1 - top_height, + } + } }, new FillFlowContainer { @@ -241,14 +247,16 @@ namespace osu.Game.Screens.Select Padding = new MarginPadding { Top = 14, Right = shear_width / 2 }, AutoSizeAxes = Axes.Both, Shear = wedged_container_shear, - Children = new[] + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] { - createStarRatingDisplay(starDifficulty).With(display => + starRatingDisplay = new StarRatingDisplay(default, animated: true) { - display.Anchor = Anchor.TopRight; - display.Origin = Anchor.TopRight; - display.Shear = -wedged_container_shear; - }), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Shear = -wedged_container_shear, + Alpha = 0f, + }, StatusPill = new BeatmapSetOnlineStatusPill { Anchor = Anchor.TopRight, @@ -304,6 +312,18 @@ namespace osu.Game.Screens.Select titleBinding.BindValueChanged(_ => setMetadata(metadata.Source)); artistBinding.BindValueChanged(_ => setMetadata(metadata.Source), true); + starRatingDisplay.DisplayedStars.BindValueChanged(s => + { + difficultyColourBar.Colour = colours.ForStarDifficulty(s.NewValue); + }, true); + + starDifficulty = difficultyCache.GetBindableDifficulty(beatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); + starDifficulty.BindValueChanged(s => + { + starRatingDisplay.FadeIn(transition_duration); + starRatingDisplay.Current.Value = s.NewValue ?? default; + }); + // no difficulty means it can't have a status to show if (beatmapInfo.Version == null) StatusPill.Hide(); @@ -311,13 +331,6 @@ namespace osu.Game.Screens.Select addInfoLabels(); } - private static Drawable createStarRatingDisplay(StarDifficulty difficulty) => difficulty.Stars > 0 - ? new StarRatingDisplay(difficulty) - { - Margin = new MarginPadding { Bottom = 5 } - } - : Empty(); - private void setMetadata(string source) { ArtistLabel.Text = artistBinding.Value; @@ -429,6 +442,7 @@ namespace osu.Game.Screens.Select { base.Dispose(isDisposing); settingChangeTracker?.Dispose(); + cancellationSource?.Cancel(); } public class InfoLabel : Container, IHasTooltip @@ -489,43 +503,6 @@ namespace osu.Game.Screens.Select }; } } - - private class DifficultyColourBar : Container - { - private readonly StarDifficulty difficulty; - - public DifficultyColourBar(StarDifficulty difficulty) - { - this.difficulty = difficulty; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - const float full_opacity_ratio = 0.7f; - - var difficultyColour = colours.ForStarDifficulty(difficulty.Stars); - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = difficultyColour, - Width = full_opacity_ratio, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, - Colour = difficultyColour, - Alpha = 0.5f, - X = full_opacity_ratio, - Width = 1 - full_opacity_ratio, - } - }; - } - } } } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index ea55fd28c2..0f805990b9 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -105,12 +105,18 @@ namespace osu.Game.Skinning /// Returns a list of all usable s that have been loaded by the user. /// /// A newly allocated list of available . - public List GetAllUserSkins() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + public List GetAllUserSkins(bool includeFiles = false) + { + if (includeFiles) + return ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + + return ModelStore.Items.Where(s => !s.DeletePending).ToList(); + } public void SelectRandomSkin() { // choose from only user skins, removing the current selection to ensure a new one is chosen. - var randomChoices = GetAllUsableSkins().Where(s => s.ID != CurrentSkinInfo.Value.ID).ToArray(); + var randomChoices = ModelStore.Items.Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); if (randomChoices.Length == 0) { @@ -118,7 +124,8 @@ namespace osu.Game.Skinning return; } - CurrentSkinInfo.Value = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); + var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); + CurrentSkinInfo.Value = ModelStore.ConsumableItems.Single(i => i.ID == chosen.ID); } protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name }; diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs index 03ab94d1da..d7ab769ac4 100644 --- a/osu.Game/Tests/CleanRunHeadlessGameHost.cs +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.cs @@ -2,14 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System.Runtime.CompilerServices; -using osu.Framework.Platform; +using osu.Framework.Testing; namespace osu.Game.Tests { /// /// A headless host which cleans up before running (removing any remnants from a previous execution). /// - public class CleanRunHeadlessGameHost : HeadlessGameHost + public class CleanRunHeadlessGameHost : TestRunHeadlessGameHost { /// /// Create a new instance. diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index f2da66d666..2c0ca0b872 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Task JoinRoom(long roomId, string? password = null) { - var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == roomId); + var apiRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == roomId); if (password != apiRoom.Password.Value) throw new InvalidOperationException("Invalid password."); @@ -260,7 +260,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Debug.Assert(Room != null); - var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == Room.RoomID); + var apiRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == Room.RoomID); var set = apiRoom.Playlist.FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet ?? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId)?.BeatmapSet; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestRequestHandlingMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestRequestHandlingMultiplayerRoomManager.cs index 2e56c8a094..5de518990a 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestRequestHandlingMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestRequestHandlingMultiplayerRoomManager.cs @@ -1,160 +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; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Game.Online.API; -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; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { /// - /// A for use in multiplayer test scenes. Should generally not be used by itself outside of a . + /// A for use in multiplayer test scenes, backed by a . + /// Should generally not be used by itself outside of a . /// - /// - /// This implementation will pretend to be a server, handling room retrieval and manipulation requests - /// and returning a roughly expected state, without the need for a server to be running. - /// public class TestRequestHandlingMultiplayerRoomManager : MultiplayerRoomManager { - [Resolved] - private IAPIProvider api { get; set; } + public IReadOnlyList ServerSideRooms => handler.ServerSideRooms; - [Resolved] - private OsuGameBase game { get; set; } - - [Cached] - public readonly Bindable Filter = new Bindable(new FilterCriteria()); - - public new readonly List Rooms = new List(); - - private int currentRoomId; - private int currentPlaylistItemId; + private readonly TestRoomRequestsHandler handler = new TestRoomRequestsHandler(); [BackgroundDependencyLoader] - private void load() + private void load(IAPIProvider api, OsuGameBase game) { - int currentScoreId = 0; - - // Handling here is pretending to be a server, while also updating the local state to match - // how the server would eventually respond and update the RoomManager. - ((DummyAPIAccess)api).HandleRequest = req => - { - switch (req) - { - case CreateRoomRequest createRoomRequest: - var apiRoom = new Room(); - - apiRoom.CopyFrom(createRoomRequest.Room); - - // Passwords are explicitly not copied between rooms. - apiRoom.HasPassword.Value = !string.IsNullOrEmpty(createRoomRequest.Room.Password.Value); - apiRoom.Password.Value = createRoomRequest.Room.Password.Value; - - AddRoom(apiRoom); - - var responseRoom = new APICreatedRoom(); - responseRoom.CopyFrom(createResponseRoom(apiRoom, false)); - - createRoomRequest.TriggerSuccess(responseRoom); - return true; - - case JoinRoomRequest joinRoomRequest: - { - var room = Rooms.Single(r => r.RoomID.Value == joinRoomRequest.Room.RoomID.Value); - - if (joinRoomRequest.Password != room.Password.Value) - { - joinRoomRequest.TriggerFailure(new InvalidOperationException("Invalid password.")); - return true; - } - - joinRoomRequest.TriggerSuccess(); - return true; - } - - case PartRoomRequest partRoomRequest: - partRoomRequest.TriggerSuccess(); - return true; - - case GetRoomsRequest getRoomsRequest: - var roomsWithoutParticipants = new List(); - - foreach (var r in Rooms) - roomsWithoutParticipants.Add(createResponseRoom(r, false)); - - getRoomsRequest.TriggerSuccess(roomsWithoutParticipants); - return true; - - case GetRoomRequest getRoomRequest: - getRoomRequest.TriggerSuccess(createResponseRoom(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId), true)); - return true; - - case GetBeatmapSetRequest getBeatmapSetRequest: - var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type); - onlineReq.Success += res => getBeatmapSetRequest.TriggerSuccess(res); - onlineReq.Failure += e => getBeatmapSetRequest.TriggerFailure(e); - - // Get the online API from the game's dependencies. - game.Dependencies.Get().Queue(onlineReq); - return true; - - case CreateRoomScoreRequest createRoomScoreRequest: - createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 }); - return true; - - case SubmitRoomScoreRequest submitRoomScoreRequest: - submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore - { - ID = currentScoreId++, - Accuracy = 1, - EndedAt = DateTimeOffset.Now, - Passed = true, - Rank = ScoreRank.S, - MaxCombo = 1000, - TotalScore = 1000000, - User = api.LocalUser.Value, - Statistics = new Dictionary() - }); - return true; - } - - return false; - }; + ((DummyAPIAccess)api).HandleRequest = request => handler.HandleRequest(request, api.LocalUser.Value, game); } - public void AddRoom(Room room) - { - room.RoomID.Value ??= currentRoomId++; - for (int i = 0; i < room.Playlist.Count; i++) - room.Playlist[i].ID = currentPlaylistItemId++; - - Rooms.Add(room); - } - - public new void RemoveRoom(Room room) => base.RemoveRoom(room); - - private Room createResponseRoom(Room room, bool withParticipants) - { - var responseRoom = new Room(); - responseRoom.CopyFrom(room); - responseRoom.Password.Value = null; - if (!withParticipants) - responseRoom.RecentParticipants.Clear(); - return responseRoom; - } - - public new void ClearRooms() => base.ClearRooms(); - - public new void Schedule(Action action) => base.Schedule(action); + /// + /// Adds a room to a local "server-side" list that's returned when a is fired. + /// + /// The room. + public void AddServerSideRoom(Room room) => handler.AddServerSideRoom(room); } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 6e1e831d9b..71acefb158 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -4,7 +4,6 @@ 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 { @@ -23,11 +22,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// IRoomManager RoomManager { get; } - /// - /// The cached . - /// - Bindable Filter { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 997c910dd4..8716646074 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -9,7 +9,6 @@ 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 { @@ -20,7 +19,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay { 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; diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index 05ba509a73..defc971eef 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Tests.Visual.OnlinePlay { @@ -20,7 +19,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay { public Bindable SelectedRoom { get; } public IRoomManager RoomManager { get; } - public Bindable Filter { get; } public OngoingOperationTracker OngoingOperationTracker { get; } public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } @@ -36,7 +34,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay { SelectedRoom = new Bindable(); RoomManager = CreateRoomManager(); - Filter = new Bindable(new FilterCriteria()); OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); @@ -44,7 +41,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay CacheAs(SelectedRoom); CacheAs(RoomManager); - CacheAs(Filter); CacheAs(OngoingOperationTracker); CacheAs(AvailabilityTracker); CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); @@ -75,6 +71,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay drawableComponents.Add(drawable); } - protected virtual IRoomManager CreateRoomManager() => new BasicTestRoomManager(); + protected virtual IRoomManager CreateRoomManager() => new TestRequestHandlingRoomManager(); } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRequestHandlingRoomManager.cs similarity index 52% rename from osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs rename to osu.Game/Tests/Visual/OnlinePlay/TestRequestHandlingRoomManager.cs index d37a64fa4b..d88fd68b20 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRequestHandlingRoomManager.cs @@ -2,12 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; -using osu.Framework.Bindables; +using osu.Framework.Allocation; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Users; @@ -16,46 +15,24 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// /// A very simple for use in online play test scenes. /// - public class BasicTestRoomManager : IRoomManager + public class TestRequestHandlingRoomManager : RoomManager { - public event Action RoomsUpdated; - - public readonly BindableList Rooms = new BindableList(); - public Action JoinRoomRequested; - public IBindable InitialRoomsReceived { get; } = new Bindable(true); + private int currentRoomId; - IBindableList IRoomManager.Rooms => Rooms; + private readonly TestRoomRequestsHandler handler = new TestRoomRequestsHandler(); - public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + [BackgroundDependencyLoader] + private void load(IAPIProvider api, OsuGameBase game) { - room.RoomID.Value ??= Rooms.Select(r => r.RoomID.Value).Where(id => id != null).Select(id => id.Value).DefaultIfEmpty().Max() + 1; - onSuccess?.Invoke(room); - - AddRoom(room); + ((DummyAPIAccess)api).HandleRequest = request => handler.HandleRequest(request, api.LocalUser.Value, game); } - public void AddRoom(Room room) - { - Rooms.Add(room); - RoomsUpdated?.Invoke(); - } - - public void RemoveRoom(Room room) - { - Rooms.Remove(room); - RoomsUpdated?.Invoke(); - } - - public void JoinRoom(Room room, string password, Action onSuccess = null, Action onError = null) + public override void JoinRoom(Room room, string password = null, Action onSuccess = null, Action onError = null) { JoinRoomRequested?.Invoke(room, password); - onSuccess?.Invoke(room); - } - - public void PartRoom() - { + base.JoinRoom(room, password, onSuccess, onError); } public void AddRooms(int count, RulesetInfo ruleset = null, bool withPassword = false) @@ -64,15 +41,16 @@ namespace osu.Game.Tests.Visual.OnlinePlay { var room = new Room { - RoomID = { Value = i }, - Position = { Value = i }, - Name = { Value = $"Room {i}" }, - Host = { Value = new User { Username = "Host" } }, + RoomID = { Value = -currentRoomId }, + Name = { Value = $@"Room {currentRoomId}" }, + Host = { Value = new User { Username = @"Host" } }, EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }, Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal }, - Password = { Value = withPassword ? "password" : string.Empty } }; + if (withPassword) + room.Password.Value = @"password"; + if (ruleset != null) { room.Playlist.Add(new PlaylistItem @@ -89,6 +67,8 @@ namespace osu.Game.Tests.Visual.OnlinePlay } CreateRoom(room); + + currentRoomId++; } } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs new file mode 100644 index 0000000000..7f975c9985 --- /dev/null +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -0,0 +1,147 @@ +// 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.Allocation; +using osu.Game.Online.API; +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.Users; + +namespace osu.Game.Tests.Visual.OnlinePlay +{ + /// + /// Represents a handler which pretends to be a server, handling room retrieval and manipulation requests + /// and returning a roughly expected state, without the need for a server to be running. + /// + public class TestRoomRequestsHandler + { + public IReadOnlyList ServerSideRooms => serverSideRooms; + + private readonly List serverSideRooms = new List(); + + private int currentRoomId; + private int currentPlaylistItemId; + private int currentScoreId; + + /// + /// Handles an API request, while also updating the local state to match + /// how the server would eventually respond and update an . + /// + /// The API request to handle. + /// The local user to store in responses where required. + /// The game base for cases where actual online requests need to be sent. + /// Whether the request was successfully handled. + public bool HandleRequest(APIRequest request, User localUser, OsuGameBase game) + { + switch (request) + { + case CreateRoomRequest createRoomRequest: + var apiRoom = new Room(); + + apiRoom.CopyFrom(createRoomRequest.Room); + + // Passwords are explicitly not copied between rooms. + apiRoom.HasPassword.Value = !string.IsNullOrEmpty(createRoomRequest.Room.Password.Value); + apiRoom.Password.Value = createRoomRequest.Room.Password.Value; + + AddServerSideRoom(apiRoom); + + var responseRoom = new APICreatedRoom(); + responseRoom.CopyFrom(createResponseRoom(apiRoom, false)); + + createRoomRequest.TriggerSuccess(responseRoom); + return true; + + case JoinRoomRequest joinRoomRequest: + { + var room = ServerSideRooms.Single(r => r.RoomID.Value == joinRoomRequest.Room.RoomID.Value); + + if (joinRoomRequest.Password != room.Password.Value) + { + joinRoomRequest.TriggerFailure(new InvalidOperationException("Invalid password.")); + return true; + } + + joinRoomRequest.TriggerSuccess(); + return true; + } + + case PartRoomRequest partRoomRequest: + partRoomRequest.TriggerSuccess(); + return true; + + case GetRoomsRequest getRoomsRequest: + var roomsWithoutParticipants = new List(); + + foreach (var r in ServerSideRooms) + roomsWithoutParticipants.Add(createResponseRoom(r, false)); + + getRoomsRequest.TriggerSuccess(roomsWithoutParticipants); + return true; + + case GetRoomRequest getRoomRequest: + getRoomRequest.TriggerSuccess(createResponseRoom(ServerSideRooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId), true)); + return true; + + case GetBeatmapSetRequest getBeatmapSetRequest: + var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type); + onlineReq.Success += res => getBeatmapSetRequest.TriggerSuccess(res); + onlineReq.Failure += e => getBeatmapSetRequest.TriggerFailure(e); + + // Get the online API from the game's dependencies. + game.Dependencies.Get().Queue(onlineReq); + return true; + + case CreateRoomScoreRequest createRoomScoreRequest: + createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 }); + return true; + + case SubmitRoomScoreRequest submitRoomScoreRequest: + submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.S, + MaxCombo = 1000, + TotalScore = 1000000, + User = localUser, + Statistics = new Dictionary() + }); + return true; + } + + return false; + } + + /// + /// Adds a room to a local "server-side" list that's returned when a is fired. + /// + /// The room. + public void AddServerSideRoom(Room room) + { + room.RoomID.Value ??= currentRoomId++; + for (int i = 0; i < room.Playlist.Count; i++) + room.Playlist[i].ID = currentPlaylistItemId++; + + serverSideRooms.Add(room); + } + + private Room createResponseRoom(Room room, bool withParticipants) + { + var responseRoom = new Room(); + responseRoom.CopyFrom(room); + responseRoom.Password.Value = null; + if (!withParticipants) + responseRoom.RecentParticipants.Clear(); + return responseRoom; + } + } +} diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs similarity index 92% rename from osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs rename to osu.Game/Tests/Visual/OsuGameTestScene.cs index c9a1471e41..f38aaa9358 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -22,9 +22,8 @@ using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Menu; using osuTK.Graphics; -using IntroSequence = osu.Game.Configuration.IntroSequence; -namespace osu.Game.Tests.Visual.Navigation +namespace osu.Game.Tests.Visual { /// /// A scene which tests full game flow. @@ -62,7 +61,7 @@ namespace osu.Game.Tests.Visual.Navigation Game.Dispose(); } - RecycleLocalStorage(); + RecycleLocalStorage(false); CreateGame(); }); @@ -73,15 +72,18 @@ namespace osu.Game.Tests.Visual.Navigation ConfirmAtMainMenu(); } + [TearDownSteps] + public void TearDownSteps() + { + AddStep("exit game", () => Game.Exit()); + AddUntilStep("wait for game exit", () => Game.Parent == null); + } + protected void CreateGame() { Game = new TestOsuGame(LocalStorage, API); Game.SetHost(host); - // todo: this can be removed once we can run audio tracks without a device present - // see https://github.com/ppy/osu/issues/1302 - Game.LocalConfig.SetValue(OsuSetting.IntroSequence, IntroSequence.Circles); - Add(Game); } diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 57e400a77e..ef9181c8a6 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -100,7 +99,7 @@ namespace osu.Game.Tests.Visual return factory; }); - RecycleLocalStorage(); + RecycleLocalStorage(false); var baseDependencies = base.CreateChildDependencies(parent); @@ -140,7 +139,7 @@ namespace osu.Game.Tests.Visual protected virtual bool UseFreshStoragePerRun => false; - public virtual void RecycleLocalStorage() + public virtual void RecycleLocalStorage(bool isDisposing) { if (localStorage?.IsValueCreated == true) { @@ -155,7 +154,7 @@ namespace osu.Game.Tests.Visual } localStorage = - new Lazy(() => isolatedHostStorage ?? new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); + new Lazy(() => isolatedHostStorage ?? new TemporaryNativeStorage($"{GetType().Name}-{Guid.NewGuid()}")); } [Resolved] @@ -199,7 +198,7 @@ namespace osu.Game.Tests.Visual if (contextFactory?.IsValueCreated == true) contextFactory.Value.ResetDatabase(); - RecycleLocalStorage(); + RecycleLocalStorage(true); } protected override ITestSceneTestRunner CreateRunner() => new OsuTestSceneTestRunner(); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d4dba9330f..0abd9adf77 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,8 +36,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/osu.iOS.props b/osu.iOS.props index 7e514afe74..d03985a0c2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,8 +70,8 @@ - - + + @@ -93,7 +93,7 @@ - + diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 139ee02b96..e42b30e944 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -105,8 +105,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -304,6 +305,7 @@ AABB API BPM + FPS GC GL GLSL