diff --git a/osu.Android.props b/osu.Android.props index eff0eed278..17b5cb67e9 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,8 +51,8 @@ - - + + diff --git a/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs new file mode 100644 index 0000000000..d6ef390a8f --- /dev/null +++ b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Desktop.LegacyIpc +{ + /// + /// A difficulty calculation request from the legacy client. + /// + /// + /// Synchronise any changes with osu!stable. + /// + public class LegacyIpcDifficultyCalculationRequest + { + public string BeatmapFile { get; set; } + public int RulesetId { get; set; } + public int Mods { get; set; } + } +} diff --git a/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs new file mode 100644 index 0000000000..7b9fae5797 --- /dev/null +++ b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Desktop.LegacyIpc +{ + /// + /// A difficulty calculation response returned to the legacy client. + /// + /// + /// Synchronise any changes with osu!stable. + /// + public class LegacyIpcDifficultyCalculationResponse + { + public double StarRating { get; set; } + } +} diff --git a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs new file mode 100644 index 0000000000..0fa60e2068 --- /dev/null +++ b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs @@ -0,0 +1,53 @@ +// 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.Platform; +using Newtonsoft.Json.Linq; + +namespace osu.Desktop.LegacyIpc +{ + /// + /// An that can be used to communicate to and from legacy clients. + /// + /// In order to deserialise types at either end, types must be serialised as their , + /// however this cannot be done since osu!stable and osu!lazer live in two different assemblies. + ///
+ /// To get around this, this class exists which serialises a payload () as an type, + /// which can be deserialised at either end because it is part of the core library (mscorlib / System.Private.CorLib). + /// The payload contains the data to be sent over the IPC channel. + ///
+ /// At either end, Json.NET deserialises the payload into a which is manually converted back into the expected type, + /// which then further contains another representing the data sent over the IPC channel whose type can likewise be lazily matched through + /// . + ///
+ ///
+ /// + /// Synchronise any changes with osu-stable. + /// + public class LegacyIpcMessage : IpcMessage + { + public LegacyIpcMessage() + { + // Types/assemblies are not inter-compatible, so always serialise/deserialise into objects. + base.Type = typeof(object).FullName; + } + + public new string Type => base.Type; // Hide setter. + + public new object Value + { + get => base.Value; + set => base.Value = new Data + { + MessageType = value.GetType().Name, + MessageData = value + }; + } + + public class Data + { + public string MessageType { get; set; } + public object MessageData { get; set; } + } + } +} diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs new file mode 100644 index 0000000000..97a4c57bf0 --- /dev/null +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -0,0 +1,121 @@ +// 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 Newtonsoft.Json.Linq; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; + +#nullable enable + +namespace osu.Desktop.LegacyIpc +{ + /// + /// Provides IPC to legacy osu! clients. + /// + public class LegacyTcpIpcProvider : TcpIpcProvider + { + private static readonly Logger logger = Logger.GetLogger("legacy-ipc"); + + public LegacyTcpIpcProvider() + : base(45357) + { + MessageReceived += msg => + { + try + { + logger.Add("Processing legacy IPC message..."); + logger.Add($" {msg.Value}", LogLevel.Debug); + + // See explanation in LegacyIpcMessage for why this is done this way. + var legacyData = ((JObject)msg.Value).ToObject(); + object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType); + + return new LegacyIpcMessage + { + Value = onLegacyIpcMessageReceived(value) + }; + } + catch (Exception ex) + { + logger.Add($"Processing IPC message failed: {msg.Value}", exception: ex); + return null; + } + }; + } + + private object parseObject(JObject value, string type) + { + switch (type) + { + case nameof(LegacyIpcDifficultyCalculationRequest): + return value.ToObject() + ?? throw new InvalidOperationException($"Failed to parse request {value}"); + + case nameof(LegacyIpcDifficultyCalculationResponse): + return value.ToObject() + ?? throw new InvalidOperationException($"Failed to parse request {value}"); + + default: + throw new ArgumentException($"Unsupported object type {type}"); + } + } + + private object onLegacyIpcMessageReceived(object message) + { + switch (message) + { + case LegacyIpcDifficultyCalculationRequest req: + try + { + var ruleset = getLegacyRulesetFromID(req.RulesetId); + + Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray(); + WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset); + + return new LegacyIpcDifficultyCalculationResponse + { + StarRating = ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods).StarRating + }; + } + catch + { + return new LegacyIpcDifficultyCalculationResponse(); + } + + default: + throw new ArgumentException($"Unsupported message type {message}"); + } + } + + private static Ruleset getLegacyRulesetFromID(int rulesetId) + { + switch (rulesetId) + { + case 0: + return new OsuRuleset(); + + case 1: + return new TaikoRuleset(); + + case 2: + return new CatchRuleset(); + + case 3: + return new ManiaRuleset(); + + default: + throw new ArgumentException("Invalid ruleset id"); + } + } + } +} diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 898f7d5105..a9e3575a49 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using osu.Desktop.LegacyIpc; using osu.Framework; using osu.Framework.Development; using osu.Framework.Logging; @@ -18,8 +19,10 @@ namespace osu.Desktop { private const string base_game_name = @"osu"; + private static LegacyTcpIpcProvider legacyIpc; + [STAThread] - public static int Main(string[] args) + public static void Main(string[] args) { // Back up the cwd before DesktopGameHost changes it string cwd = Environment.CurrentDirectory; @@ -69,14 +72,29 @@ namespace osu.Desktop throw new TimeoutException(@"IPC took too long to send"); } - return 0; + return; } // we want to allow multiple instances to be started when in debug. if (!DebugUtils.IsDebugBuild) { Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error); - return 0; + return; + } + } + + if (host.IsPrimaryInstance) + { + try + { + Logger.Log("Starting legacy IPC provider..."); + legacyIpc = new LegacyTcpIpcProvider(); + legacyIpc.Bind(); + legacyIpc.StartAsync(); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to start legacy IPC provider"); } } @@ -84,8 +102,6 @@ namespace osu.Desktop host.Run(new TournamentGame()); else host.Run(new OsuGameDesktop(args)); - - return 0; } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 9d1f5429a1..1aa20f4737 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.UI; @@ -46,12 +45,6 @@ namespace osu.Game.Rulesets.Mania.Edit [Resolved] private EditorBeatmap beatmap { get; set; } - [Resolved] - private IScrollingInfo scrollingInfo { get; set; } - - [Resolved] - private Bindable working { get; set; } - [Resolved] private OsuColour colours { get; set; } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index dc858fb54f..9fe1eb7932 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -7,16 +7,12 @@ using osu.Framework.Allocation; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.Mania.Edit { public class ManiaSelectionHandler : EditorSelectionHandler { - [Resolved] - private IScrollingInfo scrollingInfo { get; set; } - [Resolved] private HitObjectComposer composer { get; set; } diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs index 90d3c6c4c7..9f4963b022 100644 --- a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs @@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.Mania.UI public JudgementResult Result { get; private set; } - [Resolved] - private Column column { get; set; } - private SkinnableDrawable skinnableExplosion; public PoolableHitExplosion() diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 861b800038..16be20f7f3 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -12,7 +12,6 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -149,9 +148,6 @@ namespace osu.Game.Rulesets.Taiko.UI centreHit.Colour = colours.Pink; } - [Resolved(canBeNull: true)] - private GameplayClock gameplayClock { get; set; } - public bool OnPressed(KeyBindingPressEvent e) { Drawable target = null; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 6e5a546e87..a73ae9dcdb 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -2,14 +2,20 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; using osu.Game.Scoring; @@ -21,6 +27,14 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestFixture] public class LegacyScoreDecoderTest { + private CultureInfo originalCulture; + + [SetUp] + public void SetUp() + { + originalCulture = CultureInfo.CurrentCulture; + } + [Test] public void TestDecodeManiaReplay() { @@ -44,6 +58,59 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestCultureInvariance() + { + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = new TestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset); + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton) + } + } + }; + + // the "se" culture is used here, as it encodes the negative number sign as U+2212 MINUS SIGN, + // rather than the classic ASCII U+002D HYPHEN-MINUS. + CultureInfo.CurrentCulture = new CultureInfo("se"); + + var encodeStream = new MemoryStream(); + + var encoder = new LegacyScoreEncoder(score, beatmap); + encoder.Encode(encodeStream); + + var decodeStream = new MemoryStream(encodeStream.GetBuffer()); + + var decoder = new TestLegacyScoreDecoder(); + var decodedAfterEncode = decoder.Parse(decodeStream); + + Assert.Multiple(() => + { + Assert.That(decodedAfterEncode, Is.Not.Null); + + Assert.That(decodedAfterEncode.ScoreInfo.User.Username, Is.EqualTo(scoreInfo.User.Username)); + Assert.That(decodedAfterEncode.ScoreInfo.BeatmapInfoID, Is.EqualTo(scoreInfo.BeatmapInfoID)); + Assert.That(decodedAfterEncode.ScoreInfo.Ruleset, Is.EqualTo(scoreInfo.Ruleset)); + Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(scoreInfo.TotalScore)); + Assert.That(decodedAfterEncode.ScoreInfo.MaxCombo, Is.EqualTo(scoreInfo.MaxCombo)); + Assert.That(decodedAfterEncode.ScoreInfo.Date, Is.EqualTo(scoreInfo.Date)); + + Assert.That(decodedAfterEncode.Replay.Frames.Count, Is.EqualTo(1)); + }); + } + + [TearDown] + public void TearDown() + { + CultureInfo.CurrentCulture = originalCulture; + } + private class TestLegacyScoreDecoder : LegacyScoreDecoder { private static readonly Dictionary rulesets = new Ruleset[] diff --git a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs index 2a60a7b96d..3a82cbc785 100644 --- a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs @@ -25,9 +25,6 @@ namespace osu.Game.Tests.Beatmaps private BeatmapSetInfo importedSet; - [Resolved] - private BeatmapManager beatmaps { get; set; } - private TestBeatmapDifficultyCache difficultyCache; private IBindable starDifficultyBindable; diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index f4e0838be1..cc7e8a0c97 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -45,9 +45,9 @@ namespace osu.Game.Tests.Database { var rulesets = new RealmRulesetStore(realmFactory, storage); - Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false); - Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false); - Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false); + Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged); + Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged); + Assert.IsFalse(rulesets.GetRuleset("mania")?.IsManaged); }); } } diff --git a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs index b612899d79..28937b2120 100644 --- a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs +++ b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs @@ -18,9 +18,6 @@ namespace osu.Game.Tests.Input [Resolved] private FrameworkConfigManager frameworkConfigManager { get; set; } - [Resolved] - private OsuConfigManager osuConfigManager { get; set; } - [TestCase(WindowMode.Windowed)] [TestCase(WindowMode.Borderless)] public void TestDisableConfining(WindowMode windowMode) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 840ff20a83..42305ccd81 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -16,6 +16,27 @@ namespace osu.Game.Tests.NonVisual.Multiplayer [HeadlessTest] public class StatefulMultiplayerClientTest : MultiplayerTestScene { + [Test] + public void TestUserAddedOnJoin() + { + var user = new APIUser { Id = 33 }; + + AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + } + + [Test] + public void TestUserRemovedOnLeave() + { + var user = new APIUser { Id = 44 }; + + AddStep("add user", () => Client.AddUser(user)); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + + AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); + AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); + } + [Test] public void TestPlayingUserTracking() { diff --git a/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs deleted file mode 100644 index 5a621ecf84..0000000000 --- a/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using osu.Framework.Testing; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Tests.Visual.Multiplayer; - -namespace osu.Game.Tests.OnlinePlay -{ - [HeadlessTest] - public class StatefulMultiplayerClientTest : MultiplayerTestScene - { - [Test] - public void TestUserAddedOnJoin() - { - var user = new APIUser { Id = 33 }; - - AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); - } - - [Test] - public void TestUserRemovedOnLeave() - { - var user = new APIUser { Id = 44 }; - - AddStep("add user", () => Client.AddUser(user)); - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); - - AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); - AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); - } - } -} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index a0b27755b7..a0602e21b9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -2,12 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Skinning; using osu.Game.Skinning.Editor; namespace osu.Game.Tests.Visual.Gameplay @@ -16,9 +14,6 @@ namespace osu.Game.Tests.Visual.Gameplay { private SkinEditor skinEditor; - [Resolved] - private SkinManager skinManager { get; set; } - protected override bool Autoplay => true; [SetUpSteps] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 723e35ed55..3074a91dc6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -10,7 +10,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; -using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -36,9 +35,6 @@ namespace osu.Game.Tests.Visual.Gameplay private Drawable hideTarget => hudOverlay.KeyCounter; private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First(); - [Resolved] - private OsuConfigManager config { get; set; } - [Test] public void TestComboCounterIncrementing() { diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 57d60cea9e..c65595d82e 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Menus private TestToolbar toolbar; [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } [SetUp] public void SetUp() => Schedule(() => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index d66603a448..1d61a5d496 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -2,11 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Beatmaps; using osu.Game.Online.Rooms; -using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Components; @@ -18,12 +15,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene { - [Resolved] - private BeatmapManager beatmapManager { get; set; } - - [Resolved] - private RulesetStore rulesetStore { get; set; } - [SetUp] public new void Setup() => Schedule(() => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 4521a7fa0f..2411f39ae3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -397,6 +397,44 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); } + [Test] + public void TestPlayStartsWithCorrectBeatmapWhileAtSongSelect() + { + createRoom(() => 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("Enter song select", () => + { + var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen; + + ((MultiplayerMatchSubScreen)currentSubScreen).SelectBeatmap(); + }); + + AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); + + AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); + + AddStep("Select next beatmap", () => InputManager.Key(Key.Down)); + + AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != client.Room?.Playlist.First().BeatmapID); + + AddStep("start match externally", () => client.StartMatch()); + + AddUntilStep("play started", () => multiplayerScreenStack.CurrentScreen is Player); + + AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); + } + [Test] public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs new file mode 100644 index 0000000000..674ee0f186 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -0,0 +1,234 @@ +// 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.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerPlaylist : MultiplayerTestScene + { + private MultiplayerPlaylist list; + private BeatmapManager beatmaps; + private RulesetStore rulesets; + private BeatmapSetInfo importedSet; + private BeatmapInfo importedBeatmap; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + } + + [SetUp] + public new void Setup() => Schedule(() => + { + Child = list = new MultiplayerPlaylist + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.4f, 0.8f) + }; + }); + + [SetUpSteps] + public new void SetUpSteps() + { + AddStep("import beatmap", () => + { + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); + }); + + AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + } + + [Test] + public void TestNonExpiredItemsAddedToQueueList() + { + assertItemInQueueListStep(1, 0); + + addItemStep(); + assertItemInQueueListStep(2, 1); + + addItemStep(); + assertItemInQueueListStep(3, 2); + } + + [Test] + public void TestExpiredItemsAddedToHistoryList() + { + assertItemInQueueListStep(1, 0); + + addItemStep(true); + assertItemInHistoryListStep(2, 0); + + addItemStep(true); + assertItemInHistoryListStep(3, 0); + assertItemInHistoryListStep(2, 1); + + // Initial item is still in the queue. + assertItemInQueueListStep(1, 0); + } + + [Test] + public void TestExpiredItemsMoveToQueueList() + { + addItemStep(); + addItemStep(); + + AddStep("finish current item", () => Client.FinishCurrentItem()); + + assertItemInHistoryListStep(1, 0); + assertItemInQueueListStep(2, 0); + assertItemInQueueListStep(3, 1); + + AddStep("finish current item", () => Client.FinishCurrentItem()); + + assertItemInHistoryListStep(2, 0); + assertItemInHistoryListStep(1, 1); + assertItemInQueueListStep(3, 0); + + AddStep("finish current item", () => Client.FinishCurrentItem()); + + assertItemInHistoryListStep(3, 0); + assertItemInHistoryListStep(2, 1); + assertItemInHistoryListStep(1, 2); + } + + [Test] + public void TestListsClearedWhenRoomLeft() + { + addItemStep(); + AddStep("finish current item", () => Client.FinishCurrentItem()); + + AddStep("leave room", () => RoomManager.PartRoom()); + AddUntilStep("wait for room part", () => Client.Room == null); + + AddUntilStep("item 0 not in lists", () => !inHistoryList(0) && !inQueueList(0)); + AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0)); + } + + [Ignore("Expired items are initially removed from the room.")] + [Test] + public void TestJoinRoomWithMixedItemsAddedInCorrectLists() + { + AddStep("leave room", () => RoomManager.PartRoom()); + AddUntilStep("wait for room part", () => Client.Room == null); + + AddStep("join room with items", () => + { + RoomManager.CreateRoom(new Room + { + Name = { Value = "test name" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + }, + new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value }, + Expired = true + } + } + }); + }); + + AddUntilStep("wait for room join", () => RoomJoined); + + assertItemInQueueListStep(1, 0); + assertItemInHistoryListStep(2, 0); + } + + /// + /// Adds a step to create a new playlist item. + /// + private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem + { + Beatmap = { Value = importedBeatmap }, + BeatmapID = importedBeatmap.OnlineID ?? -1, + Expired = expired, + PlayedAt = DateTimeOffset.Now + }))); + + /// + /// Asserts the position of a given playlist item in the queue list. + /// + /// The item id. + /// The index at which the item should appear visually. The item with index 0 is at the top of the list. + private void assertItemInQueueListStep(int playlistItemId, int visualIndex) + { + changeDisplayModeStep(MultiplayerPlaylistDisplayMode.Queue); + + AddUntilStep($"{playlistItemId} in queue at pos = {visualIndex}", () => + { + return !inHistoryList(playlistItemId) + && this.ChildrenOfType() + .Single() + .ChildrenOfType() + .OrderBy(drawable => drawable.Position.Y) + .TakeWhile(drawable => drawable.Item.ID != playlistItemId) + .Count() == visualIndex; + }); + } + + /// + /// Asserts the position of a given playlist item in the history list. + /// + /// The item id. + /// The index at which the item should appear visually. The item with index 0 is at the top of the list. + private void assertItemInHistoryListStep(int playlistItemId, int visualIndex) + { + changeDisplayModeStep(MultiplayerPlaylistDisplayMode.History); + + AddUntilStep($"{playlistItemId} in history at pos = {visualIndex}", () => + { + return !inQueueList(playlistItemId) + && this.ChildrenOfType() + .Single() + .ChildrenOfType() + .OrderBy(drawable => drawable.Position.Y) + .TakeWhile(drawable => drawable.Item.ID != playlistItemId) + .Count() == visualIndex; + }); + } + + private void changeDisplayModeStep(MultiplayerPlaylistDisplayMode mode) => AddStep($"change list to {mode}", () => list.DisplayMode.Value = mode); + + private bool inQueueList(int playlistItemId) + { + return this.ChildrenOfType() + .Single() + .Items.Any(i => i.ID == playlistItemId); + } + + private bool inHistoryList(int playlistItemId) + { + return this.ChildrenOfType() + .Single() + .Items.Any(i => i.ID == playlistItemId); + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 35c66e8cda..5aac228f4b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -24,9 +24,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestScenePlaylistsSongSelect : OnlinePlayTestScene { - [Resolved] - private BeatmapManager beatmapManager { get; set; } - private BeatmapManager manager; private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs new file mode 100644 index 0000000000..9e684e4f10 --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs @@ -0,0 +1,99 @@ +// 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.Configuration; +using osu.Game.Screens.Play; +using osu.Game.Tests.Beatmaps.IO; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneMouseWheelVolumeAdjust : OsuGameTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + // Headless tests are always at minimum volume. This covers interactive tests, matching that initial value. + AddStep("Set volume to min", () => Game.Audio.Volume.Value = 0); + AddAssert("Volume is min", () => Game.Audio.AggregateVolume.Value == 0); + AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); + } + + [Test] + public void TestAdjustVolumeFromMainMenu() + { + // First scroll makes volume controls appear, second adjusts volume. + AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2); + AddUntilStep("Volume is above zero", () => Game.Audio.AggregateVolume.Value > 0); + } + + [Test] + public void TestAdjustVolumeFromPlayerWheelEnabled() + { + loadToPlayerNonBreakTime(); + + // First scroll makes volume controls appear, second adjusts volume. + AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2); + AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0); + } + + [Test] + public void TestAdjustVolumeFromPlayerWheelDisabled() + { + AddStep("disable wheel volume adjust", () => Game.LocalConfig.SetValue(OsuSetting.MouseDisableWheel, true)); + + loadToPlayerNonBreakTime(); + + // First scroll makes volume controls appear, second adjusts volume. + AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 2); + AddAssert("Volume is still zero", () => Game.Audio.Volume.Value == 0); + } + + [Test] + public void TestAdjustVolumeFromPlayerWheelDisabledHoldingAlt() + { + AddStep("disable wheel volume adjust", () => Game.LocalConfig.SetValue(OsuSetting.MouseDisableWheel, true)); + + loadToPlayerNonBreakTime(); + + // First scroll makes volume controls appear, second adjusts volume. + AddRepeatStep("Adjust volume using mouse wheel holding alt", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.ScrollVerticalBy(5); + InputManager.ReleaseKey(Key.AltLeft); + }, 2); + + AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0); + } + + private void loadToPlayerNonBreakTime() + { + Player player = null; + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + // dismiss any notifications that may appear (ie. muted notification). + clickMouseInCentre(); + return (player = Game.ScreenStack.CurrentScreen as Player) != null; + }); + + AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value); + } + + private void clickMouseInCentre() + { + InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Left); + } + } +} diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index 2706ff5ceb..4d1e279090 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -83,9 +83,6 @@ namespace osu.Game.Tests.Visual.Navigation [Resolved] private OsuGameBase gameBase { get; set; } - [Resolved] - private GameHost host { get; set; } - [Test] public void TestNullRulesetHandled() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs index 90f3eb64e4..63741451f3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Online } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } [Test] public void TestMultipleRulesetsBeatmapSet() diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 3314e291e8..f87cca80b0 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Online } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } [Test] public void TestLoading() diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 7028ecf39f..9c65b2dc51 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; -using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -47,9 +46,6 @@ namespace osu.Game.Tests.Visual.Online [CanBeNull] private Func> onGetMessages; - [Resolved] - private GameHost host { get; set; } - public TestSceneChatOverlay() { channels = Enumerable.Range(1, 10) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 19e06beaad..52d5eb2c65 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Online private TestUserListPanel evast; [Resolved] - private RulesetStore rulesetStore { get; set; } + private IRulesetStore rulesetStore { get; set; } [SetUp] public void SetUp() => Schedule(() => diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index ce8136199f..1c92bb1e38 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -4,8 +4,6 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Profile; @@ -20,9 +18,6 @@ namespace osu.Game.Tests.Visual.Online private readonly TestUserProfileOverlay profile; - [Resolved] - private IAPIProvider api { get; set; } - public static readonly APIUser TEST_USER = new APIUser { Username = @"Somebody", diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index acacdf8644..f246560c82 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; -using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking; @@ -20,9 +19,6 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneContractedPanelMiddleContent : OsuTestScene { - [Resolved] - private RulesetStore rulesetStore { get; set; } - [Test] public void TestShowPanel() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index e5bcc08924..ede89c6096 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -135,6 +135,35 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("bpm is default", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60)); } + [TestCase(true)] + [TestCase(false)] + public void TestEarlyActivationEffectPoint(bool earlyActivating) + { + double earlyActivationMilliseconds = earlyActivating ? 100 : 0; + ControlPoint actualEffectPoint = null; + + AddStep($"set early activation to {earlyActivationMilliseconds}", () => beatContainer.EarlyActivationMilliseconds = earlyActivationMilliseconds); + + AddStep("seek before kiai effect point", () => + { + ControlPoint expectedEffectPoint = Beatmap.Value.Beatmap.ControlPointInfo.EffectPoints.First(ep => ep.KiaiMode); + actualEffectPoint = null; + beatContainer.AllowMistimedEventFiring = false; + + beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => + { + if (Precision.AlmostEquals(gameplayClockContainer.CurrentTime + earlyActivationMilliseconds, expectedEffectPoint.Time, BeatSyncedContainer.MISTIMED_ALLOWANCE)) + actualEffectPoint = effectControlPoint; + }; + + gameplayClockContainer.Seek(expectedEffectPoint.Time - earlyActivationMilliseconds); + }); + + AddUntilStep("wait for effect point", () => actualEffectPoint != null); + + AddAssert("effect has kiai", () => actualEffectPoint != null && ((EffectControlPoint)actualEffectPoint).KiaiMode); + } + private class TestBeatSyncedContainer : BeatSyncedContainer { private const int flash_layer_height = 150; @@ -145,6 +174,12 @@ namespace osu.Game.Tests.Visual.UserInterface set => base.AllowMistimedEventFiring = value; } + public new double EarlyActivationMilliseconds + { + get => base.EarlyActivationMilliseconds; + set => base.EarlyActivationMilliseconds = value; + } + private readonly InfoString timingPointCount; private readonly InfoString currentTimingPoint; private readonly InfoString beatCount; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs index d30f1e8889..3fa9b8b877 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs @@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface private BeatmapSetInfo testBeatmap; private IAPIProvider api; - [Resolved] - private BeatmapManager beatmaps { get; set; } - [BackgroundDependencyLoader] private void load(OsuGameBase osu, IAPIProvider api, RulesetStore rulesets) { diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs index 8139387a96..b678f69b8f 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs @@ -6,19 +6,20 @@ using osu.Framework.Graphics; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; +using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; namespace osu.Game.Tournament.Tests.Components { public class TestSceneTournamentBeatmapPanel : TournamentTestScene { + /// + /// Warning: the below API instance is actually the online API, rather than the dummy API provided by the test. + /// It cannot be trivially replaced because setting to causes to no longer be usable. + /// [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs index 3cd13df0d3..9feef36a02 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tournament.Tests.Components private IAPIProvider api { get; set; } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } private FillFlowContainer fillFlow; diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index d149ec145b..3619aae7e0 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tournament.Tests.NonVisual Directory.CreateDirectory(flagsPath); // Define testing files corresponding to the specific file migrations that are needed - string bracketFile = Path.Combine(osuRoot, "bracket.json"); + string bracketFile = Path.Combine(osuRoot, TournamentGameBase.BRACKET_FILENAME); string drawingsConfig = Path.Combine(osuRoot, "drawings.ini"); string drawingsFile = Path.Combine(osuRoot, "drawings.txt"); @@ -133,7 +133,7 @@ namespace osu.Game.Tournament.Tests.NonVisual Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath)); - Assert.True(storage.Exists("bracket.json")); + Assert.True(storage.Exists(TournamentGameBase.BRACKET_FILENAME)); Assert.True(storage.Exists("drawings.txt")); Assert.True(storage.Exists("drawings_results.txt")); diff --git a/osu.Game.Tournament/Components/TournamentModIcon.cs b/osu.Game.Tournament/Components/TournamentModIcon.cs index 0fde263bc8..ed8a36c220 100644 --- a/osu.Game.Tournament/Components/TournamentModIcon.cs +++ b/osu.Game.Tournament/Components/TournamentModIcon.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Components private readonly string modAcronym; [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } public TournamentModIcon(string modAcronym) { diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 02cf567837..347d368a04 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tournament.IO DeleteRecursive(source); } - moveFileIfExists("bracket.json", destination); + moveFileIfExists(TournamentGameBase.BRACKET_FILENAME, destination); moveFileIfExists("drawings.txt", destination); moveFileIfExists("drawings_results.txt", destination); moveFileIfExists("drawings.ini", destination); diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index a57f9fd691..5278d538d2 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tournament.IPC protected IAPIProvider API { get; private set; } [Resolved] - protected RulesetStore Rulesets { get; private set; } + protected IRulesetStore Rulesets { get; private set; } [Resolved] private GameHost host { get; set; } diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 9abf1d3adb..5d2fddffd9 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -25,9 +25,6 @@ namespace osu.Game.Tournament.Screens.Editors protected override BindableList Storage => team.SeedingResults; - [Resolved(canBeNull: true)] - private TournamentSceneManager sceneManager { get; set; } - public SeedingEditorScreen(TournamentTeam team, TournamentScreen parentScreen) : base(parentScreen) { @@ -38,9 +35,6 @@ namespace osu.Game.Tournament.Screens.Editors { public SeedingResult Model { get; } - [Resolved] - private LadderInfo ladderInfo { get; set; } - public SeedingResultRow(TournamentTeam team, SeedingResult round) { Model = round; diff --git a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs index 8e9b32231f..5a1ceecd01 100644 --- a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs @@ -21,9 +21,6 @@ namespace osu.Game.Tournament.Screens.Setup { public class StablePathSelectScreen : TournamentScreen { - [Resolved] - private GameHost host { get; set; } - [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index f03f815b83..5d613894d4 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tournament loadingSpinner.Expire(); Logger.Error(t.Exception, "Couldn't load bracket with error"); - Add(new WarningBox("Your bracket.json file could not be parsed. Please check runtime.log for more details.")); + Add(new WarningBox($"Your {BRACKET_FILENAME} file could not be parsed. Please check runtime.log for more details.")); }); return; diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index d2f146c4c2..d08322a3e8 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.IO.Stores; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Graphics; using osu.Game.Online.API.Requests; @@ -26,15 +27,15 @@ namespace osu.Game.Tournament [Cached(typeof(TournamentGameBase))] public class TournamentGameBase : OsuGameBase { - private const string bracket_filename = "bracket.json"; + public const string BRACKET_FILENAME = @"bracket.json"; private LadderInfo ladder; private TournamentStorage storage; private DependencyContainer dependencies; private FileBasedIPC ipc; - protected Task BracketLoadTask => taskCompletionSource.Task; + protected Task BracketLoadTask => bracketLoadTaskCompletionSource.Task; - private readonly TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); + private readonly TaskCompletionSource bracketLoadTaskCompletionSource = new TaskCompletionSource(); protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -71,9 +72,9 @@ namespace osu.Game.Tournament { try { - if (storage.Exists(bracket_filename)) + if (storage.Exists(BRACKET_FILENAME)) { - using (Stream stream = storage.GetStream(bracket_filename, FileAccess.Read, FileMode.Open)) + using (Stream stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Read, FileMode.Open)) using (var sr = new StreamReader(stream)) ladder = JsonConvert.DeserializeObject(sr.ReadToEnd(), new JsonPointConverter()); } @@ -144,7 +145,7 @@ namespace osu.Game.Tournament } catch (Exception e) { - taskCompletionSource.SetException(e); + bracketLoadTaskCompletionSource.SetException(e); return; } @@ -156,7 +157,7 @@ namespace osu.Game.Tournament dependencies.CacheAs(ipc = new FileBasedIPC()); Add(ipc); - taskCompletionSource.SetResult(true); + bracketLoadTaskCompletionSource.SetResult(true); initialisationText.Expire(); }); @@ -292,6 +293,12 @@ namespace osu.Game.Tournament protected virtual void SaveChanges() { + if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully) + { + Logger.Log("Inhibiting bracket save as bracket parsing failed"); + return; + } + foreach (var r in ladder.Rounds) r.Matches = ladder.Matches.Where(p => p.Round.Value == r).Select(p => p.ID).ToList(); @@ -309,7 +316,7 @@ namespace osu.Game.Tournament Converters = new JsonConverter[] { new JsonPointConverter() } }); - using (var stream = storage.GetStream(bracket_filename, FileAccess.Write, FileMode.Create)) + using (var stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Write, FileMode.Create)) using (var sw = new StreamWriter(stream)) sw.Write(serialisedLadder); } diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index e631d35180..6d56d152f1 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -18,9 +18,6 @@ namespace osu.Game.Audio private readonly BindableDouble muteBindable = new BindableDouble(); - [Resolved] - private AudioManager audio { get; set; } - private ITrackStore trackStore; protected TrackManagerPreviewTrack CurrentTrack; diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index 8b00d0f7f2..3949e84f4a 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps private IAPIProvider api { get; set; } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } [Resolved] private Bindable ruleset { get; set; } @@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps /// private int? requestedUserId; - private readonly Dictionary recommendedDifficultyMapping = new Dictionary(); + private readonly Dictionary recommendedDifficultyMapping = new Dictionary(); private readonly IBindable apiState = new Bindable(); @@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps /// Rulesets ordered descending by their respective recommended difficulties. /// The currently selected ruleset will always be first. /// - private IEnumerable orderedRulesets + private IEnumerable orderedRulesets { get { diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 6e573cc2a0..82be0559a7 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps.Drawables } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs new file mode 100644 index 0000000000..8c915e2872 --- /dev/null +++ b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs @@ -0,0 +1,52 @@ +// 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.IO; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Skinning; + +namespace osu.Game.Beatmaps +{ + /// + /// A which can be constructed directly from a .osu file, providing an implementation for + /// . + /// + public class FlatFileWorkingBeatmap : WorkingBeatmap + { + private readonly Beatmap beatmap; + + public FlatFileWorkingBeatmap(string file, Func rulesetProvider, int? beatmapId = null) + : this(readFromFile(file), rulesetProvider, beatmapId) + { + } + + private FlatFileWorkingBeatmap(Beatmap beatmap, Func rulesetProvider, int? beatmapId = null) + : base(beatmap.BeatmapInfo, null) + { + this.beatmap = beatmap; + + beatmap.BeatmapInfo.Ruleset = rulesetProvider(beatmap.BeatmapInfo.RulesetID).RulesetInfo; + + if (beatmapId.HasValue) + beatmap.BeatmapInfo.OnlineID = beatmapId; + } + + private static Beatmap readFromFile(string filename) + { + using (var stream = File.OpenRead(filename)) + using (var reader = new LineBufferedReader(stream)) + return Decoder.GetDecoder(reader).Decode(reader); + } + + protected override IBeatmap GetBeatmap() => beatmap; + protected override Texture GetBackground() => throw new NotImplementedException(); + protected override Track GetBeatmapTrack() => throw new NotImplementedException(); + protected internal override ISkin GetSkin() => throw new NotImplementedException(); + public override Stream GetStream(string storagePath) => throw new NotImplementedException(); + } +} diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index ad23874b2e..77bda00107 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osuTK; @@ -193,9 +192,6 @@ namespace osu.Game.Collections [NotNull] protected new CollectionFilterMenuItem Item => ((DropdownMenuItem)base.Item).Value; - [Resolved] - private OsuColour colours { get; set; } - [Resolved] private IBindable beatmap { get; set; } diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 9ff92032b7..c4f991094c 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -40,9 +40,6 @@ namespace osu.Game.Collections public readonly BindableList Collections = new BindableList(); - [Resolved] - private GameHost host { get; set; } - [Resolved] private BeatmapManager beatmaps { get; set; } diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 6e4901ab1a..2024d18570 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -111,7 +111,7 @@ namespace osu.Game.Graphics.Containers if (clock == null) return; - double currentTrackTime = clock.CurrentTime; + double currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded) { @@ -132,13 +132,11 @@ namespace osu.Game.Graphics.Containers { // this may be the case where the beat syncing clock has been paused. // we still want to show an idle animation, so use this container's time instead. - currentTrackTime = Clock.CurrentTime; + currentTrackTime = Clock.CurrentTime + EarlyActivationMilliseconds; timingPoint = TimingControlPoint.DEFAULT; effectPoint = EffectControlPoint.DEFAULT; } - currentTrackTime += EarlyActivationMilliseconds; - double beatLength = timingPoint.BeatLength / Divisor; while (beatLength < MinimumBeatLength) diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs index d9d0e4c0ea..f381aad39a 100644 --- a/osu.Game/IPC/ArchiveImportIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -18,6 +18,7 @@ namespace osu.Game.IPC : base(host) { this.importer = importer; + MessageReceived += msg => { Debug.Assert(importer != null); @@ -25,6 +26,8 @@ namespace osu.Game.IPC { if (t.Exception != null) throw t.Exception; }, TaskContinuationOptions.OnlyOnFaulted); + + return null; }; } diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index 5e894c4e0b..fd7225ad2e 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.cs @@ -35,9 +35,14 @@ namespace osu.Game.Localisation public static LocalisableString ConfineMouseMode => new TranslatableString(getKey(@"confine_mouse_mode"), @"Confine mouse cursor to window"); /// - /// "Disable mouse wheel during gameplay" + /// "Disable mouse wheel adjusting volume during gameplay" /// - public static LocalisableString DisableMouseWheel => new TranslatableString(getKey(@"disable_mouse_wheel"), @"Disable mouse wheel during gameplay"); + public static LocalisableString DisableMouseWheelVolumeAdjust => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust"), @"Disable mouse wheel adjusting volume during gameplay"); + + /// + /// "Volume can still be adjusted using the mouse wheel by holding "Alt"" + /// + public static LocalisableString DisableMouseWheelVolumeAdjustTooltip => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust_tooltip"), @"Volume can still be adjusted using the mouse wheel by holding ""Alt"""); /// /// "Disable mouse buttons during gameplay" diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Models/RealmRuleset.cs index 9a7488fda2..b959d0b4dc 100644 --- a/osu.Game/Models/RealmRuleset.cs +++ b/osu.Game/Models/RealmRuleset.cs @@ -50,6 +50,8 @@ namespace osu.Game.Models public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + public bool Equals(IRulesetInfo? other) => other is RealmRuleset b && Equals(b); + public override string ToString() => Name; public RealmRuleset Clone() => new RealmRuleset diff --git a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs index 6cd45a41df..671f543422 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs @@ -26,9 +26,12 @@ namespace osu.Game.Online.API.Requests { var request = base.CreateWebRequest(); - request.AddParameter(@"id", beatmapInfo.OnlineID.ToString()); - request.AddParameter(@"checksum", beatmapInfo.MD5Hash); - request.AddParameter(@"filename", filename); + if (beatmapInfo.OnlineID > 0) + request.AddParameter(@"id", beatmapInfo.OnlineID.ToString()); + if (!string.IsNullOrEmpty(beatmapInfo.MD5Hash)) + request.AddParameter(@"checksum", beatmapInfo.MD5Hash); + if (!string.IsNullOrEmpty(filename)) + request.AddParameter(@"filename", filename); return request; } diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs index e32451fc2f..28da5222f9 100644 --- a/osu.Game/Online/API/Requests/GetUserRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRequest.cs @@ -9,7 +9,7 @@ namespace osu.Game.Online.API.Requests public class GetUserRequest : APIRequest { public readonly string Lookup; - public readonly RulesetInfo Ruleset; + public readonly IRulesetInfo Ruleset; private readonly LookupType lookupType; /// @@ -24,7 +24,7 @@ namespace osu.Game.Online.API.Requests /// /// The user to get. /// The ruleset to get the user's info for. - public GetUserRequest(long? userId = null, RulesetInfo ruleset = null) + public GetUserRequest(long? userId = null, IRulesetInfo ruleset = null) { Lookup = userId.ToString(); lookupType = LookupType.Id; @@ -36,7 +36,7 @@ namespace osu.Game.Online.API.Requests /// /// The user to get. /// The ruleset to get the user's info for. - public GetUserRequest(string username = null, RulesetInfo ruleset = null) + public GetUserRequest(string username = null, IRulesetInfo ruleset = null) { Lookup = username; lookupType = LookupType.Username; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index e01c7c9e49..644c2e2a99 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -65,9 +65,6 @@ namespace osu.Game.Online.Leaderboards [Resolved(CanBeNull = true)] private SongSelect songSelect { get; set; } - [Resolved] - private ScoreManager scoreManager { get; set; } - [Resolved] private Storage storage { get; set; } diff --git a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs index ab4210251e..3db497bd6a 100644 --- a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs +++ b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs @@ -3,13 +3,11 @@ using System; using System.Threading; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets; using osuTK; namespace osu.Game.Online.Leaderboards @@ -25,9 +23,6 @@ namespace osu.Game.Online.Leaderboards protected override bool StartHidden => true; - [Resolved] - private RulesetStore rulesets { get; set; } - public UserTopScoreContainer(Func createScoreDelegate) { this.createScoreDelegate = createScoreDelegate; diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 0822c29376..3ddd6ce62b 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -32,12 +32,36 @@ namespace osu.Game.Online.Multiplayer /// public event Action? RoomUpdated; + /// + /// Invoked when a new user joins the room. + /// public event Action? UserJoined; + /// + /// Invoked when a user leaves the room of their own accord. + /// public event Action? UserLeft; + /// + /// Invoked when a user was kicked from the room forcefully. + /// public event Action? UserKicked; + /// + /// Invoked when a new item is added to the playlist. + /// + public event Action? ItemAdded; + + /// + /// Invoked when a playlist item is removed from the playlist. The provided long is the playlist's item ID. + /// + public event Action? ItemRemoved; + + /// + /// Invoked when a playlist item's details change. + /// + public event Action? ItemChanged; + /// /// Invoked when the multiplayer server requests the current beatmap to be loaded into play. /// @@ -94,7 +118,7 @@ namespace osu.Game.Online.Multiplayer protected IAPIProvider API { get; private set; } = null!; [Resolved] - protected RulesetStore Rulesets { get; private set; } = null!; + protected IRulesetStore Rulesets { get; private set; } = null!; [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; @@ -617,6 +641,7 @@ namespace osu.Game.Online.Multiplayer Room.Playlist.Add(item); APIRoom.Playlist.Add(playlistItem); + ItemAdded?.Invoke(item); RoomUpdated?.Invoke(); }); } @@ -636,6 +661,7 @@ namespace osu.Game.Online.Multiplayer Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId)); APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId); + ItemRemoved?.Invoke(playlistItemId); RoomUpdated?.Invoke(); }); @@ -666,6 +692,7 @@ namespace osu.Game.Online.Multiplayer if (CurrentMatchPlayingItem.Value?.ID == playlistItem.ID) CurrentMatchPlayingItem.Value = playlistItem; + ItemChanged?.Invoke(item); RoomUpdated?.Invoke(); }); } @@ -706,6 +733,9 @@ namespace osu.Game.Online.Multiplayer var apiBeatmap = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false); var ruleset = Rulesets.GetRuleset(item.RulesetID); + + Debug.Assert(ruleset != null); + var rulesetInstance = ruleset.CreateInstance(); var playlistItem = new PlaylistItem @@ -714,7 +744,9 @@ namespace osu.Game.Online.Multiplayer OwnerID = item.OwnerID, Beatmap = { Value = apiBeatmap }, Ruleset = { Value = ruleset }, - Expired = item.Expired + Expired = item.Expired, + PlaylistOrder = item.PlaylistOrder, + PlayedAt = item.PlayedAt }; playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 6ca0b822f3..cee6d8fe41 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -39,6 +39,22 @@ namespace osu.Game.Online.Rooms [Key(7)] public bool Expired { get; set; } + /// + /// The order in which this will be played relative to others. + /// Playlist items should be played in increasing order (lower values are played first). + /// + /// + /// This is only valid for items which are not . The value for expired items is undefined and should not be used. + /// + [Key(8)] + public ushort PlaylistOrder { get; set; } + + /// + /// The date when this was played. + /// + [Key(9)] + public DateTimeOffset? PlayedAt { get; set; } + public MultiplayerPlaylistItem() { } @@ -52,6 +68,8 @@ namespace osu.Game.Online.Rooms RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(); AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray(); Expired = item.Expired; + PlaylistOrder = item.PlaylistOrder ?? 0; + PlayedAt = item.PlayedAt; } } } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index a1480865b8..b8700fd067 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using Newtonsoft.Json; using osu.Framework.Bindables; @@ -33,6 +34,12 @@ namespace osu.Game.Online.Rooms [JsonProperty("expired")] public bool Expired { get; set; } + [JsonProperty("playlist_order")] + public ushort? PlaylistOrder { get; set; } + + [JsonProperty("played_at")] + public DateTimeOffset? PlayedAt { get; set; } + [JsonIgnore] public IBindable Valid => valid; @@ -79,11 +86,13 @@ namespace osu.Game.Online.Rooms public void MarkInvalid() => valid.Value = false; - public void MapObjects(RulesetStore rulesets) + public void MapObjects(IRulesetStore rulesets) { Beatmap.Value ??= apiBeatmap; Ruleset.Value ??= rulesets.GetRuleset(RulesetID); + Debug.Assert(Ruleset.Value != null); + Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); if (allowedModsBacking != null) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index ec296a03de..2e266e32ff 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -196,6 +196,7 @@ namespace osu.Game runMigrations(); dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); + dependencies.CacheAs(RulesetStore); dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", contextFactory)); diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 38f2bdb34f..f5b4785264 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -15,7 +15,6 @@ using osu.Framework.Threading; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -61,9 +60,6 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - public BeatmapListingFilterControl() { RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs index a9723c9c62..25aed4c980 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.BeatmapSet } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } private void onRulesetChanged(ValueChangedEvent ruleset) { @@ -57,8 +57,13 @@ namespace osu.Game.Overlays.BeatmapSet if (ruleset.NewValue == null) return; + var rulesetInstance = rulesets.GetRuleset(ruleset.NewValue.OnlineID)?.CreateInstance(); + + if (rulesetInstance == null) + return; + modsContainer.Add(new ModButton(new ModNoMod())); - modsContainer.AddRange(rulesets.GetRuleset(ruleset.NewValue.OnlineID).CreateInstance().AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m))); + modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m))); modsContainer.ForEach(button => { diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index fa5a7c66d0..b9d3854066 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,7 +11,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.Comments; -using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -24,9 +22,6 @@ namespace osu.Game.Overlays public const float Y_PADDING = 25; public const float RIGHT_WIDTH = 275; - [Resolved] - private RulesetStore rulesets { get; set; } - private readonly Bindable beatmapSet = new Bindable(); // receive input outside our bounds so we can trigger a close event on ourselves. diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 269ed81bb5..0844975906 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -43,9 +43,6 @@ namespace osu.Game.Overlays.Dashboard }; } - [Resolved] - private IAPIProvider api { get; set; } - [Resolved] private UserLookupCache users { get; set; } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index affe9ecb0c..130ae44273 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -1,21 +1,20 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osuTK; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Online.API; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics; using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osuTK; namespace osu.Game.Overlays.Profile.Sections { @@ -24,9 +23,6 @@ namespace osu.Game.Overlays.Profile.Sections [Resolved] private IAPIProvider api { get; set; } - [Resolved] - protected RulesetStore Rulesets { get; private set; } - protected int VisiblePages; protected int ItemsPerPage; diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index cb8dae0bbc..7a27c6e4e1 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private IAPIProvider api { get; set; } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } private readonly APIRecentActivity activity; diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index cc553ad361..a37f762532 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -1,21 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; -using osu.Framework.Bindables; -using osu.Game.Rulesets; -using osu.Framework.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; -using osuTK; -using osu.Framework.Allocation; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Overlays.Rankings.Tables; using System.Linq; using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.Rankings.Tables; +using osu.Game.Rulesets; +using osuTK; namespace osu.Game.Overlays.Rankings { @@ -29,9 +29,6 @@ namespace osu.Game.Overlays.Rankings [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - private CancellationTokenSource cancellationToken; private GetSpotlightRankingsRequest getRankingsRequest; private GetSpotlightsRequest spotlightsRequest; diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 0334167759..4235dc0a05 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -67,7 +67,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, new SettingsCheckbox { - LabelText = MouseSettingsStrings.DisableMouseWheel, + LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust, + TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip, Current = osuConfig.GetBindable(OsuSetting.MouseDisableWheel) }, new SettingsCheckbox diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs index 6f0b433acb..789ed457a4 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs @@ -5,19 +5,14 @@ using System.Linq; using Markdig.Extensions.Yaml; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers.Markdown; using osu.Game.Graphics.Containers.Markdown; -using osu.Game.Online.API; namespace osu.Game.Overlays.Wiki.Markdown { public class WikiMarkdownContainer : OsuMarkdownContainer { - [Resolved] - private IAPIProvider api { get; set; } - public string CurrentPath { set => DocumentUrl = value; diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index 4e529a73fb..6599e0d59d 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Database; #nullable enable @@ -10,7 +11,7 @@ namespace osu.Game.Rulesets /// /// A representation of a ruleset's metadata. /// - public interface IRulesetInfo : IHasOnlineID + public interface IRulesetInfo : IHasOnlineID, IEquatable { /// /// The user-exposed name of this ruleset. diff --git a/osu.Game/Rulesets/IRulesetStore.cs b/osu.Game/Rulesets/IRulesetStore.cs new file mode 100644 index 0000000000..08d907810b --- /dev/null +++ b/osu.Game/Rulesets/IRulesetStore.cs @@ -0,0 +1,31 @@ +// 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; + +#nullable enable + +namespace osu.Game.Rulesets +{ + public interface IRulesetStore + { + /// + /// Retrieve a ruleset using a known ID. + /// + /// The ruleset's internal ID. + /// A ruleset, if available, else null. + IRulesetInfo? GetRuleset(int id); + + /// + /// Retrieve a ruleset using a known short name. + /// + /// The ruleset's short name. + /// A ruleset, if available, else null. + IRulesetInfo? GetRuleset(string shortName); + + /// + /// All available rulesets. + /// + IEnumerable AvailableRulesets { get; } + } +} diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 4a146c05bf..d018cc4194 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -49,6 +49,8 @@ namespace osu.Game.Rulesets public override bool Equals(object obj) => obj is RulesetInfo rulesetInfo && Equals(rulesetInfo); + public bool Equals(IRulesetInfo other) => other is RulesetInfo b && Equals(b); + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] public override int GetHashCode() { diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 6dd036c0e6..5cc6a75f43 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -13,7 +13,7 @@ using osu.Game.Database; namespace osu.Game.Rulesets { - public class RulesetStore : DatabaseBackedStore, IDisposable + public class RulesetStore : DatabaseBackedStore, IRulesetStore, IDisposable { private const string ruleset_library_prefix = "osu.Game.Rulesets"; @@ -236,5 +236,13 @@ namespace osu.Game.Rulesets { AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly; } + + #region Implementation of IRulesetStore + + IRulesetInfo IRulesetStore.GetRuleset(int id) => GetRuleset(id); + IRulesetInfo IRulesetStore.GetRuleset(string shortName) => GetRuleset(shortName); + IEnumerable IRulesetStore.AvailableRulesets => AvailableRulesets; + + #endregion } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 52aecb27de..d0bbf859af 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -19,7 +19,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Skinning; using osuTK; using System.Diagnostics; -using osu.Framework.Audio.Sample; namespace osu.Game.Rulesets.UI { @@ -88,9 +87,6 @@ namespace osu.Game.Rulesets.UI [Resolved(CanBeNull = true)] private IReadOnlyList mods { get; set; } - [Resolved] - private ISampleStore sampleStore { get; set; } - /// /// Creates a new . /// diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 7b8cacb35b..3d67aa9558 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -46,7 +46,7 @@ namespace osu.Game.Scoring.Legacy sw.Write(LATEST_VERSION); sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash); sw.Write(score.ScoreInfo.UserString); - sw.Write($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}".ComputeMD5Hash()); + sw.Write(FormattableString.Invariant($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}").ComputeMD5Hash()); sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0)); sw.Write((ushort)(score.ScoreInfo.GetCount100() ?? 0)); sw.Write((ushort)(score.ScoreInfo.GetCount50() ?? 0)); @@ -110,7 +110,9 @@ namespace osu.Game.Scoring.Legacy } } - replayData.AppendFormat(@"{0}|{1}|{2}|{3},", -12345, 0, 0, 0); + // Warning: this is purposefully hardcoded as a string rather than interpolating, as in some cultures the minus sign is not encoded as the standard ASCII U+00C2 codepoint, + // which then would break decoding. + replayData.Append(@"-12345|0|0|0"); return replayData.ToString(); } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs index 4629f9b540..f0e643f805 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs @@ -1,20 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { public class GroupVisualisation : CompositeDrawable { - [Resolved] - private OsuColour colours { get; set; } - public readonly ControlPointGroup Group; private readonly IBindableList controlPoints = new BindableList(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index b8fa05e7eb..265f56534f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -279,9 +279,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline editorClock.Start(); } - [Resolved] - private EditorBeatmap beatmap { get; set; } - [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index 2b2e66fb18..9610f6424c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -1,12 +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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -16,9 +14,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly IBindableList controlPoints = new BindableList(); - [Resolved] - private OsuColour colours { get; set; } - public TimelineControlPointGroup(ControlPointGroup group) { Group = group; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 80aa6972b1..1839b0507d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -184,9 +184,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private SamplePointPiece sampleOverrideDisplay; private DifficultyPointPiece difficultyOverrideDisplay; - [Resolved] - private EditorBeatmap beatmap { get; set; } - private DifficultyControlPoint difficultyControlPoint; private SampleControlPoint sampleControlPoint; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ac71298f36..48489c60ab 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -109,9 +109,6 @@ namespace osu.Game.Screens.Edit [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private MusicController music { get; set; } - [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); diff --git a/osu.Game/Screens/Edit/EditorRoundedScreen.cs b/osu.Game/Screens/Edit/EditorRoundedScreen.cs index 7f7b3abc2a..62f40f0325 100644 --- a/osu.Game/Screens/Edit/EditorRoundedScreen.cs +++ b/osu.Game/Screens/Edit/EditorRoundedScreen.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osu.Game.Overlays; namespace osu.Game.Screens.Edit @@ -14,9 +13,6 @@ namespace osu.Game.Screens.Edit { public const int HORIZONTAL_PADDING = 100; - [Resolved] - private OsuColour colours { get; set; } - private Container roundedContent; protected override Container Content => roundedContent; diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs index ab8bd6a3bc..a67a060134 100644 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ b/osu.Game/Screens/Edit/EditorTable.cs @@ -62,9 +62,6 @@ namespace osu.Game.Screens.Edit private readonly Box hoveredBackground; - [Resolved] - private EditorClock clock { get; set; } - public RowBackground(object item) { Item = item; diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index f833bc49f7..d1e35ae20d 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Database; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osuTK; @@ -36,9 +35,6 @@ namespace osu.Game.Screens.Edit.Setup [Resolved] private OsuGameBase game { get; set; } - [Resolved] - private SectionsContainer sectionsContainer { get; set; } - public FileChooserLabelledTextBox(params string[] handledExtensions) { this.handledExtensions = handledExtensions; diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 7a98cf63c3..1e6899e05f 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -132,9 +132,6 @@ namespace osu.Game.Screens.Edit.Timing controlPoints.BindTo(group.ControlPoints); } - [Resolved] - private OsuColour colours { get; set; } - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index fd238feeac..cadcdebc6e 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -23,9 +23,6 @@ namespace osu.Game.Screens.Edit.Verify { private IssueTable table; - [Resolved] - private EditorClock clock { get; set; } - [Resolved] private IBindable workingBeatmap { get; set; } diff --git a/osu.Game/Screens/Menu/StorageErrorDialog.cs b/osu.Game/Screens/Menu/StorageErrorDialog.cs index dcaad4013a..250623ec68 100644 --- a/osu.Game/Screens/Menu/StorageErrorDialog.cs +++ b/osu.Game/Screens/Menu/StorageErrorDialog.cs @@ -15,9 +15,6 @@ namespace osu.Game.Screens.Menu [Resolved] private DialogOverlay dialogOverlay { get; set; } - [Resolved] - private OsuGameBase osuGame { get; set; } - public StorageErrorDialog(OsuStorage storage, OsuStorageError error) { HeaderText = "osu! storage error"; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index ddfdab18f7..02565c6ebe 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; -using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -28,10 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private readonly Bindable joinedRoom = new Bindable(); [Resolved] - private RulesetStore rulesets { get; set; } - - [Resolved] - private BeatmapManager beatmaps { get; set; } + private IRulesetStore rulesets { get; set; } [Resolved] private IAPIProvider api { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index b9d2bdf23e..22842fbb9e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading.Tasks; -using osu.Framework.Allocation; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components @@ -12,9 +11,6 @@ namespace osu.Game.Screens.OnlinePlay.Components /// public class SelectionPollingComponent : RoomPollingComponent { - [Resolved] - private IRoomManager roomManager { get; set; } - private readonly Room room; public SelectionPollingComponent(Room room) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 6deca0482a..f2d31c8e67 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -1,9 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; @@ -22,13 +20,11 @@ namespace osu.Game.Screens.OnlinePlay private readonly bool allowSelection; private readonly bool showItemOwner; - public DrawableRoomPlaylist(bool allowEdit, bool allowSelection, bool reverse = false, bool showItemOwner = false) + public DrawableRoomPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) { this.allowEdit = allowEdit; this.allowSelection = allowSelection; this.showItemOwner = showItemOwner; - - ((ReversibleFillFlowContainer)ListContainer).Reverse = reverse; } protected override void LoadComplete() @@ -53,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay d.ScrollbarVisible = false; }); - protected override FillFlowContainer> CreateListFillFlowContainer() => new ReversibleFillFlowContainer + protected override FillFlowContainer> CreateListFillFlowContainer() => new FillFlowContainer> { Spacing = new Vector2(0, 2) }; @@ -76,22 +72,5 @@ namespace osu.Game.Screens.OnlinePlay Items.Remove(item); } - - private class ReversibleFillFlowContainer : FillFlowContainer> - { - private bool reverse; - - public bool Reverse - { - get => reverse; - set - { - reverse = value; - Invalidate(); - } - } - - public override IEnumerable FlowingChildren => Reverse ? base.FlowingChildren.OrderBy(d => -GetLayoutPosition(d)) : base.FlowingChildren; - } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 9920883078..0502c4abe6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -30,9 +30,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public readonly Room Room; - [Resolved] - private BeatmapManager beatmaps { get; set; } - protected Container ButtonsContainer { get; private set; } private readonly Bindable roomType = new Bindable(); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 54c762b8ce..f4d7823fcc 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -33,9 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved] private IRoomManager roomManager { get; set; } - [Resolved(CanBeNull = true)] - private LoungeSubScreen loungeSubScreen { get; set; } - // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 7c5ed3f5cc..184ac2c563 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -319,6 +319,16 @@ namespace osu.Game.Screens.OnlinePlay.Match protected void StartPlay() { + // User may be at song select or otherwise when the host starts gameplay. + // Ensure that they first return to this screen, else global bindables (beatmap etc.) may be in a bad state. + if (!this.IsCurrentScreen()) + { + this.MakeCurrent(); + + Schedule(StartPlay); + return; + } + sampleStart?.Play(); // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 34edc1ccd1..39d60a0b05 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -20,7 +19,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; -using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; @@ -84,12 +82,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private MultiplayerClient client { get; set; } - [Resolved] - private Bindable beatmap { get; set; } - - [Resolved] - private Bindable ruleset { get; set; } - [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index ce988e377f..874113d859 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; -using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay.Components; using osuTK; @@ -25,9 +24,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match set => button.Action = value; } - [Resolved] - private IAPIProvider api { get; set; } - [Resolved] private OsuColour colours { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs new file mode 100644 index 0000000000..d708b39898 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.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 System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Rooms; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist +{ + /// + /// A historically-ordered list of s. + /// + public class MultiplayerHistoryList : DrawableRoomPlaylist + { + public MultiplayerHistoryList() + : base(false, false, true) + { + } + + protected override FillFlowContainer> CreateListFillFlowContainer() => new HistoryFillFlowContainer + { + Spacing = new Vector2(0, 2) + }; + + private class HistoryFillFlowContainer : FillFlowContainer> + { + public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderByDescending(item => item.Model.PlayedAt); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs new file mode 100644 index 0000000000..c3245b550f --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist +{ + /// + /// The multiplayer playlist, containing lists to show the items from a in both gameplay-order and historical-order. + /// + public class MultiplayerPlaylist : MultiplayerRoomComposite + { + public readonly Bindable DisplayMode = new Bindable(); + + private MultiplayerQueueList queueList; + private MultiplayerHistoryList historyList; + private bool firstPopulation = true; + + [BackgroundDependencyLoader] + private void load() + { + const float tab_control_height = 25; + + InternalChildren = new Drawable[] + { + new OsuTabControl + { + RelativeSizeAxes = Axes.X, + Height = tab_control_height, + Current = { BindTarget = DisplayMode } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = tab_control_height + 5 }, + Masking = true, + Children = new Drawable[] + { + queueList = new MultiplayerQueueList + { + RelativeSizeAxes = Axes.Both, + SelectedItem = { BindTarget = SelectedItem } + }, + historyList = new MultiplayerHistoryList + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + SelectedItem = { BindTarget = SelectedItem } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + DisplayMode.BindValueChanged(onDisplayModeChanged, true); + } + + private void onDisplayModeChanged(ValueChangedEvent mode) + { + historyList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.History ? 1 : 0, 100); + queueList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.Queue ? 1 : 0, 100); + } + + protected override void OnRoomUpdated() + { + base.OnRoomUpdated(); + + if (Room == null) + { + historyList.Items.Clear(); + queueList.Items.Clear(); + firstPopulation = true; + return; + } + + if (firstPopulation) + { + foreach (var item in Room.Playlist) + addItemToLists(item); + + firstPopulation = false; + } + } + + protected override void PlaylistItemAdded(MultiplayerPlaylistItem item) + { + base.PlaylistItemAdded(item); + addItemToLists(item); + } + + protected override void PlaylistItemRemoved(long item) + { + base.PlaylistItemRemoved(item); + removeItemFromLists(item); + } + + protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) + { + base.PlaylistItemChanged(item); + + removeItemFromLists(item.ID); + addItemToLists(item); + } + + private void addItemToLists(MultiplayerPlaylistItem item) + { + var apiItem = Playlist.Single(i => i.ID == item.ID); + + if (item.Expired) + historyList.Items.Add(apiItem); + else + queueList.Items.Add(apiItem); + } + + private void removeItemFromLists(long item) + { + queueList.Items.RemoveAll(i => i.ID == item); + historyList.Items.RemoveAll(i => i.ID == item); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs new file mode 100644 index 0000000000..cc3dca6a34 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistDisplayMode.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist +{ + /// + /// The type of list displayed in a . + /// + public enum MultiplayerPlaylistDisplayMode + { + Queue, + History, + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs new file mode 100644 index 0000000000..1b1b66273f --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -0,0 +1,44 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Rooms; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist +{ + /// + /// A gameplay-ordered list of s. + /// + public class MultiplayerQueueList : DrawableRoomPlaylist + { + public MultiplayerQueueList() + : base(false, false, true) + { + } + + protected override FillFlowContainer> CreateListFillFlowContainer() => new QueueFillFlowContainer + { + Spacing = new Vector2(0, 2) + }; + + private class QueueFillFlowContainer : FillFlowContainer> + { + [Resolved(typeof(Room), nameof(Room.Playlist))] + private BindableList roomPlaylist { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + roomPlaylist.BindCollectionChanged((_, __) => InvalidateLayout()); + } + + public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderBy(item => item.Model.PlaylistOrder); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 077e9cef93..3a25bd7b06 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -26,6 +26,7 @@ using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play; @@ -56,8 +57,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [CanBeNull] private IDisposable readyClickOperation; - private DrawableRoomPlaylist playlist; - public MultiplayerMatchSubScreen(Room room) : base(room) { @@ -74,9 +73,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); UserMods.BindValueChanged(onUserModsChanged); - playlist.Items.BindTo(Room.Playlist); - playlist.SelectedItem.BindTo(SelectedItem); - client.LoadRequested += onLoadRequested; client.RoomUpdated += onRoomUpdated; @@ -142,21 +138,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { RelativeSizeAxes = Axes.X, Height = 40, - Action = () => - { - if (this.IsCurrentScreen()) - this.Push(new MultiplayerMatchSongSelect(Room)); - }, + Action = SelectBeatmap, Alpha = 0 }, }, null, new Drawable[] { - playlist = new DrawableRoomPlaylist(false, false, true, true) + new MultiplayerPlaylist { - RelativeSizeAxes = Axes.Both, - }, + RelativeSizeAxes = Axes.Both + } }, new[] { @@ -228,6 +220,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } }; + internal void SelectBeatmap() + { + if (!this.IsCurrentScreen()) + return; + + this.Push(new MultiplayerMatchSongSelect(Room)); + } + protected override Drawable CreateFooter() => new MultiplayerMatchFooter { OnReadyClick = onReadyClick, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs index a380ddef25..7d2fe44c4e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs @@ -4,6 +4,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Multiplayer { @@ -23,14 +24,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Client.UserLeft += invokeUserLeft; Client.UserKicked += invokeUserKicked; Client.UserJoined += invokeUserJoined; + Client.ItemAdded += invokeItemAdded; + Client.ItemRemoved += invokeItemRemoved; + Client.ItemChanged += invokeItemChanged; OnRoomUpdated(); } private void invokeOnRoomUpdated() => Scheduler.AddOnce(OnRoomUpdated); - private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.AddOnce(UserJoined, user); - private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(UserKicked, user); - private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.AddOnce(UserLeft, user); + private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => UserJoined(user)); + private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.Add(() => UserKicked(user)); + private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => UserLeft(user)); + private void invokeItemAdded(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemAdded(item)); + private void invokeItemRemoved(long item) => Schedule(() => PlaylistItemRemoved(item)); + private void invokeItemChanged(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemChanged(item)); /// /// Invoked when a user has joined the room. @@ -56,6 +63,30 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { } + /// + /// Invoked when a playlist item is added to the room. + /// + /// The added playlist item. + protected virtual void PlaylistItemAdded(MultiplayerPlaylistItem item) + { + } + + /// + /// Invoked when a playlist item is removed from the room. + /// + /// The ID of the removed playlist item. + protected virtual void PlaylistItemRemoved(long item) + { + } + + /// + /// Invoked when a playlist item is changed in the room. + /// + /// The new playlist item, with an existing item's ID. + protected virtual void PlaylistItemChanged(MultiplayerPlaylistItem item) + { + } + /// /// Invoked when any change occurs to the multiplayer room. /// @@ -71,6 +102,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Client.UserLeft -= invokeUserLeft; Client.UserKicked -= invokeUserKicked; Client.UserJoined -= invokeUserJoined; + Client.ItemAdded -= invokeItemAdded; + Client.ItemRemoved -= invokeItemRemoved; + Client.ItemChanged -= invokeItemChanged; } base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 3152f50d3d..8fbaebadfe 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private IAPIProvider api { get; set; } [Resolved] - private RulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } private SpriteIcon crown; @@ -185,9 +185,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; // Todo: Should use the room's selected item to determine ruleset. - var ruleset = rulesets.GetRuleset(0).CreateInstance(); + var ruleset = rulesets.GetRuleset(0)?.CreateInstance(); - int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank; + int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 57d0d2c198..9ac64add9a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -36,9 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [Resolved] private OsuColour colours { get; set; } - [Resolved] - private SpectatorClient spectatorClient { get; set; } - [Resolved] private MultiplayerClient multiplayerClient { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index a18e4b45cf..19153521cd 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -40,18 +40,9 @@ namespace osu.Game.Screens.OnlinePlay [Cached] private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); - [Resolved(CanBeNull = true)] - private MusicController music { get; set; } - - [Resolved] - private OsuGameBase game { get; set; } - [Resolved] protected IAPIProvider API { get; private set; } - [Resolved(CanBeNull = true)] - private OsuLogo logo { get; set; } - protected OnlinePlayScreen() { Anchor = Anchor.Centre; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 6d2a426e70..7e045802f7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -11,7 +11,6 @@ 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; using osu.Game.Screens.OnlinePlay.Match; @@ -29,9 +28,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public override string ShortTitle => "playlist"; - [Resolved] - private IAPIProvider api { get; set; } - private readonly IBindable isIdle = new BindableBool(); private MatchLeaderboard leaderboard; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 03c95ec060..0fd76f7e25 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Screens; -using osu.Game.Beatmaps; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.Select; @@ -13,9 +11,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsSongSelect : OnlinePlaySongSelect { - [Resolved] - private BeatmapManager beatmaps { get; set; } - public PlaylistsSongSelect(Room room) : base(room) { diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index 324e5d43b5..06b53e8426 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -9,9 +9,6 @@ namespace osu.Game.Screens.Play.HUD { public class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable { - [Resolved(canBeNull: true)] - private HUDOverlay hud { get; set; } - public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index 6d87211ddc..52f86d2bc3 100644 --- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -15,9 +15,6 @@ namespace osu.Game.Screens.Play.HUD { public class DefaultComboCounter : RollingCounter, ISkinnableDrawable { - [Resolved(canBeNull: true)] - private HUDOverlay hud { get; set; } - public bool UsesFixedAnchor { get; set; } public DefaultComboCounter() diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index 87b19e8433..6af89404e0 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -16,9 +16,6 @@ namespace osu.Game.Screens.Play.HUD Origin = Anchor.TopCentre; } - [Resolved(canBeNull: true)] - private HUDOverlay hud { get; set; } - public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 5c5b66d496..4859f1b977 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -40,9 +40,6 @@ namespace osu.Game.Screens.Play.HUD private bool isRolling; - [Resolved] - private ISkinSource skin { get; set; } - private readonly Container counterContainer; /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4d574dea99..a0e9428cff 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -768,7 +768,15 @@ namespace osu.Game.Screens.Play Scheduler.Add(resultsDisplayDelegate); } - protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; + protected override bool OnScroll(ScrollEvent e) + { + // During pause, allow global volume adjust regardless of settings. + if (GameplayClockContainer.IsPaused.Value) + return false; + + // Block global volume adjust if the user has asked for it (special case when holding "Alt"). + return mouseWheelDisabled.Value && !e.AltPressed; + } #region Fail Logic diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 45601999a0..7fea44b3ea 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -23,12 +23,10 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Overlays.Settings; -using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Spectate; using osu.Game.Users; using osuTK; -using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Screens.Play { @@ -44,9 +42,6 @@ namespace osu.Game.Screens.Play [Resolved] private PreviewTrackManager previewTrackManager { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - [Resolved] private BeatmapManager beatmaps { get; set; } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index d5b8a4c8ea..22be91b974 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -13,7 +13,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Scoring; using osuTK; @@ -70,9 +69,6 @@ namespace osu.Game.Screens.Ranking [Resolved] private ScoreManager scoreManager { get; set; } - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } - private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); private readonly Flow flow; private readonly Scroll scroll; diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 7543c89f17..bbe0a37d8e 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -16,7 +16,6 @@ using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.BeatmapSet; -using osu.Game.Rulesets; using osu.Game.Screens.Select.Details; using osuTK; using osuTK.Graphics; @@ -38,9 +37,6 @@ namespace osu.Game.Screens.Select [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - private IBeatmapInfo beatmapInfo; private APIFailTimes failTimes; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 1fd6d8c921..872e630ba0 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -64,9 +64,6 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private ScoreManager scoreManager { get; set; } - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } - [Resolved] private IBindable ruleset { get; set; } diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index d27122aea8..340c6ed931 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -3,13 +3,11 @@ using System.Diagnostics; using JetBrains.Annotations; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; @@ -28,9 +26,6 @@ namespace osu.Game.Skinning.Editor public const float VISIBLE_TARGET_SCALE = 0.8f; - [Resolved] - private OsuColour colours { get; set; } - public SkinEditorOverlay(ScalingContainer target) { this.target = target; diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index fd5a9500d9..bdcb85456a 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -1,10 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osuTK; @@ -23,9 +21,6 @@ namespace osu.Game.Skinning Margin = new MarginPadding(10); } - [Resolved(canBeNull: true)] - private HUDOverlay hud { get; set; } - protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score) { Anchor = Anchor.TopRight, diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 3fcca74fb8..5db4f00b46 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; @@ -30,9 +29,6 @@ namespace osu.Game.Skinning private ISampleInfo sampleInfo; private SampleChannel activeChannel; - [Resolved] - private ISampleStore sampleStore { get; set; } - /// /// Creates a new with no applied . /// An can be applied later via . diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index f935adf7a5..c9e55c09aa 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -7,7 +7,6 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; @@ -43,9 +42,6 @@ namespace osu.Game.Skinning private readonly AudioContainer samplesContainer; - [Resolved] - private ISampleStore sampleStore { get; set; } - [Resolved(CanBeNull = true)] private IPooledSampleProvider samplePool { get; set; } diff --git a/osu.Game/Stores/RealmRulesetStore.cs b/osu.Game/Stores/RealmRulesetStore.cs index 0119aec9a4..93b6d29e7d 100644 --- a/osu.Game/Stores/RealmRulesetStore.cs +++ b/osu.Game/Stores/RealmRulesetStore.cs @@ -18,7 +18,7 @@ using osu.Game.Rulesets; namespace osu.Game.Stores { - public class RealmRulesetStore : IDisposable + public class RealmRulesetStore : IRulesetStore, IDisposable { private readonly RealmContextFactory realmFactory; @@ -29,9 +29,9 @@ namespace osu.Game.Stores /// /// All available rulesets. /// - public IEnumerable AvailableRulesets => availableRulesets; + public IEnumerable AvailableRulesets => availableRulesets; - private readonly List availableRulesets = new List(); + private readonly List availableRulesets = new List(); public RealmRulesetStore(RealmContextFactory realmFactory, Storage? storage = null) { @@ -64,14 +64,14 @@ namespace osu.Game.Stores /// /// The ruleset's internal ID. /// A ruleset, if available, else null. - public IRulesetInfo? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id); + public RealmRuleset? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id); /// /// Retrieve a ruleset using a known short name. /// /// The ruleset's short name. /// A ruleset, if available, else null. - public IRulesetInfo? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName); + public RealmRuleset? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName); private Assembly? resolveRulesetDependencyAssembly(object? sender, ResolveEventArgs args) { @@ -258,5 +258,13 @@ namespace osu.Game.Stores { AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly; } + + #region Implementation of IRulesetStore + + IRulesetInfo? IRulesetStore.GetRuleset(int id) => GetRuleset(id); + IRulesetInfo? IRulesetStore.GetRuleset(string shortName) => GetRuleset(shortName); + IEnumerable IRulesetStore.AvailableRulesets => AvailableRulesets; + + #endregion } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 05b7c11e34..2510ddb432 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -50,6 +50,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex]; private int currentIndex; + private long lastPlaylistItemId; + public TestMultiplayerClient(TestMultiplayerRoomManager roomManager) { this.roomManager = roomManager; @@ -145,7 +147,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ((IMultiplayerClient)this).ResultsReady(); - finishCurrentItem().Wait(); + FinishCurrentItem().Wait(); } break; @@ -169,6 +171,7 @@ namespace osu.Game.Tests.Visual.Multiplayer serverSidePlaylist.Clear(); serverSidePlaylist.AddRange(apiRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item))); + lastPlaylistItemId = serverSidePlaylist.Max(item => item.ID); var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id) { @@ -189,6 +192,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Host = localUser }; + await updatePlaylistOrder(room).ConfigureAwait(false); await updateCurrentItem(room, false).ConfigureAwait(false); RoomSetupAction?.Invoke(room); @@ -308,12 +312,14 @@ namespace osu.Game.Tests.Visual.Multiplayer if (Room.Settings.QueueMode == QueueMode.HostOnly && Room.Host?.UserID != LocalUser?.UserID) throw new InvalidOperationException("Local user is not the room host."); + item.OwnerID = userId; + switch (Room.Settings.QueueMode) { case QueueMode.HostOnly: // In host-only mode, the current item is re-used. item.ID = currentItem.ID; - item.OwnerID = currentItem.OwnerID; + item.PlaylistOrder = currentItem.PlaylistOrder; serverSidePlaylist[currentIndex] = item; await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); @@ -323,12 +329,9 @@ namespace osu.Game.Tests.Visual.Multiplayer break; default: - item.ID = serverSidePlaylist.Last().ID + 1; - item.OwnerID = userId; - - serverSidePlaylist.Add(item); - await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false); + await addItem(item).ConfigureAwait(false); + // The current item can change as a result of an item being added. For example, if all items earlier in the queue were expired. await updateCurrentItem(Room).ConfigureAwait(false); break; } @@ -385,11 +388,11 @@ namespace osu.Game.Tests.Visual.Multiplayer if (newMode == QueueMode.HostOnly && serverSidePlaylist.All(item => item.Expired)) await duplicateCurrentItem().ConfigureAwait(false); - // When changing modes, items could have been added (above) or the queueing order could have changed. + await updatePlaylistOrder(Room).ConfigureAwait(false); await updateCurrentItem(Room).ConfigureAwait(false); } - private async Task finishCurrentItem() + public async Task FinishCurrentItem() { Debug.Assert(Room != null); Debug.Assert(APIRoom != null); @@ -397,10 +400,13 @@ namespace osu.Game.Tests.Visual.Multiplayer // Expire the current playlist item. currentItem.Expired = true; + currentItem.PlayedAt = DateTimeOffset.Now; + await ((IMultiplayerClient)this).PlaylistItemChanged(currentItem).ConfigureAwait(false); + await updatePlaylistOrder(Room).ConfigureAwait(false); // In host-only mode, a duplicate playlist item will be used for the next round. - if (Room.Settings.QueueMode == QueueMode.HostOnly) + if (Room.Settings.QueueMode == QueueMode.HostOnly && serverSidePlaylist.All(item => item.Expired)) await duplicateCurrentItem().ConfigureAwait(false); await updateCurrentItem(Room).ConfigureAwait(false); @@ -408,47 +414,96 @@ namespace osu.Game.Tests.Visual.Multiplayer private async Task duplicateCurrentItem() { - Debug.Assert(Room != null); - Debug.Assert(APIRoom != null); Debug.Assert(currentItem != null); - var newItem = new MultiplayerPlaylistItem + await addItem(new MultiplayerPlaylistItem { - ID = serverSidePlaylist.Last().ID + 1, BeatmapID = currentItem.BeatmapID, BeatmapChecksum = currentItem.BeatmapChecksum, RulesetID = currentItem.RulesetID, RequiredMods = currentItem.RequiredMods, AllowedMods = currentItem.AllowedMods - }; + }).ConfigureAwait(false); + } - serverSidePlaylist.Add(newItem); - await ((IMultiplayerClient)this).PlaylistItemAdded(newItem).ConfigureAwait(false); + private async Task addItem(MultiplayerPlaylistItem item) + { + Debug.Assert(Room != null); + + item.ID = ++lastPlaylistItemId; + + serverSidePlaylist.Add(item); + await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false); + + await updatePlaylistOrder(Room).ConfigureAwait(false); } private async Task updateCurrentItem(MultiplayerRoom room, bool notify = true) { - MultiplayerPlaylistItem newItem; + // Pick the next non-expired playlist item by playlist order, or default to the most-recently-expired item. + MultiplayerPlaylistItem nextItem = serverSidePlaylist.Where(i => !i.Expired).OrderBy(i => i.PlaylistOrder).FirstOrDefault() + ?? serverSidePlaylist.OrderByDescending(i => i.PlayedAt).First(); + + currentIndex = serverSidePlaylist.IndexOf(nextItem); + + long lastItem = room.Settings.PlaylistItemId; + room.Settings.PlaylistItemId = nextItem.ID; + + if (notify && nextItem.ID != lastItem) + await ((IMultiplayerClient)this).SettingsChanged(room.Settings).ConfigureAwait(false); + } + + private async Task updatePlaylistOrder(MultiplayerRoom room) + { + List orderedActiveItems; switch (room.Settings.QueueMode) { default: - // Pick the single non-expired playlist item. - newItem = serverSidePlaylist.FirstOrDefault(i => !i.Expired) ?? serverSidePlaylist.Last(); + orderedActiveItems = serverSidePlaylist.Where(item => !item.Expired).OrderBy(item => item.ID).ToList(); break; case QueueMode.AllPlayersRoundRobin: - // Group playlist items by (user_id -> count_expired), and select the first available playlist item from a user that has available beatmaps where count_expired is the lowest. - throw new NotImplementedException(); + orderedActiveItems = new List(); + + // Todo: This could probably be more efficient, likely at the cost of increased complexity. + // Number of "expired" or "used" items per player. + Dictionary perUserCounts = serverSidePlaylist + .GroupBy(item => item.OwnerID) + .ToDictionary(group => group.Key, group => group.Count(item => item.Expired)); + + // We'll run a simulation over all items which are not expired ("unprocessed"). Expired items will not have their ordering updated. + List unprocessedItems = serverSidePlaylist.Where(item => !item.Expired).ToList(); + + // In every iteration of the simulation, pick the first available item from the user with the lowest number of items in the queue to add to the result set. + // If multiple users have the same number of items in the queue, then the item with the lowest ID is chosen. + while (unprocessedItems.Count > 0) + { + MultiplayerPlaylistItem candidateItem = unprocessedItems + .OrderBy(item => perUserCounts[item.OwnerID]) + .ThenBy(item => item.ID) + .First(); + + unprocessedItems.Remove(candidateItem); + orderedActiveItems.Add(candidateItem); + + perUserCounts[candidateItem.OwnerID]++; + } + + break; } - currentIndex = serverSidePlaylist.IndexOf(newItem); + for (int i = 0; i < orderedActiveItems.Count; i++) + { + var item = orderedActiveItems[i]; - long lastItem = room.Settings.PlaylistItemId; - room.Settings.PlaylistItemId = newItem.ID; + if (item.PlaylistOrder == i) + continue; - if (notify && newItem.ID != lastItem) - await ((IMultiplayerClient)this).SettingsChanged(room.Settings).ConfigureAwait(false); + item.PlaylistOrder = (ushort)i; + + await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); + } } } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index abcf31c007..a4586dea12 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -25,9 +25,9 @@ namespace osu.Game.Tests.Visual.OnlinePlay private readonly List serverSideRooms = new List(); - private int currentRoomId; - private int currentPlaylistItemId; - private int currentScoreId; + private int currentRoomId = 1; + private int currentPlaylistItemId = 1; + private int currentScoreId = 1; /// /// Handles an API request, while also updating the local state to match diff --git a/osu.Game/Updater/NoActionUpdateManager.cs b/osu.Game/Updater/NoActionUpdateManager.cs index 641263ed0f..8f9c4c6f16 100644 --- a/osu.Game/Updater/NoActionUpdateManager.cs +++ b/osu.Game/Updater/NoActionUpdateManager.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; -using osu.Framework.Platform; using osu.Game.Online.API; using osu.Game.Overlays.Notifications; @@ -19,9 +18,6 @@ namespace osu.Game.Updater { private string version; - [Resolved] - private GameHost host { get; set; } - [BackgroundDependencyLoader] private void load(OsuGameBase game) { diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 0874685f49..516aa80652 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -29,9 +29,9 @@ namespace osu.Game.Users { public IBeatmapInfo BeatmapInfo { get; } - public RulesetInfo Ruleset { get; } + public IRulesetInfo Ruleset { get; } - protected InGame(IBeatmapInfo beatmapInfo, RulesetInfo ruleset) + protected InGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) { BeatmapInfo = beatmapInfo; Ruleset = ruleset; @@ -42,7 +42,7 @@ namespace osu.Game.Users public class InMultiplayerGame : InGame { - public InMultiplayerGame(IBeatmapInfo beatmapInfo, RulesetInfo ruleset) + public InMultiplayerGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) : base(beatmapInfo, ruleset) { } @@ -52,7 +52,7 @@ namespace osu.Game.Users public class InPlaylistGame : InGame { - public InPlaylistGame(IBeatmapInfo beatmapInfo, RulesetInfo ruleset) + public InPlaylistGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) : base(beatmapInfo, ruleset) { } @@ -60,7 +60,7 @@ namespace osu.Game.Users public class InSoloGame : InGame { - public InSoloGame(IBeatmapInfo beatmapInfo, RulesetInfo ruleset) + public InSoloGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) : base(beatmapInfo, ruleset) { } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7cc8893d8d..53a3337c9d 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 9c21f76617..f3dc163a67 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,8 +60,8 @@ - - + + @@ -83,7 +83,7 @@ - +